Sunday, 5 July 2026

Roject — Session Summary

Editor overhaul: ContextMenu, EventSlot refactor, session persistence, tab drag fixes, panel header redesign, FileTree sub-root, Init/Pin buttons, resize handling, portrait bar move, layout persistence per user per device.

What we built

ContextMenu Class Hierarchy

Introduced a plain class hierarchy (not a Web Component) for building nested context menus: ContextMenuDirectory, ContextMenuEntry, ContextMenuReadOnlyEntry, and ContextMenuSeparator. The root directory has show(x, y) which renders lazily and positions itself in the correct viewport corner (right/left, below/above) based on available space. A clearCloseUpwards() walk was added to cancel all ancestor close timers when a submenu is entered, fixing the bug where moving the cursor from a parent menu into a child submenu closed the parent.

src/components/context-menu/ smart corner positioning clearCloseUpwards

Tab Container ⋮ Menu

Replaced the split button with a menu button that opens a ContextMenu. Options: Add > (HTML Editor, File Tree), Duplicate, Split, Close. Duplicate was fixed by storing a factory: () => HTMLElement function in each TabEntry so new instances can be created without re-using the existing element. The tab-container:add-panel event is now handled in EditorShell's setupSplitListener.

tab-container.ts TabEntry.factory tab-container:add-panel

FileTree: Add File / Add Directory

Added +F and +D buttons to the FileTree header. Clicking either resolves the target directory from the current selection, finds a free name by scanning the tree, then shows an inline overlay with an input field, Create, and Cancel. Server-side endpoints POST /:projectId/create-file and POST /:projectId/create-directory were added to server/routes/files.ts. On success the component dispatches Editor.onFilesChanged so all FileTree instances refresh.

file-tree-panel.ts server/routes/files.ts create-file / create-directory

EventSlot Refactor

Replaced all string-based document.dispatchEvent / addEventListener calls with typed EventSlot<T> instances on the Editor singleton. Slots: onDocumentOpened, onDocumentDirty, onDocumentSaved, onFilesChanged. Components call Editor.get().onDocumentOpened.addListener(…) directly — no string keys, no silent mismatches.

EventSlot<T> src/editor/Editor.ts hard coupling

Session Persistence

session-file-store was removed after it caused an EPERM: operation not permitted, rename error on Windows (atomic rename not permitted on the session file). Replaced with a custom JsonSessionStore extends session.Store that writes directly with fs.writeFileSync — no temporary file, no rename. Sessions are stored per-ID in storage/sessions/.

server/sessionStore.ts JsonSessionStore Windows EPERM fix

Tab Drag State Preservation

Dragging a tab between containers re-attaches the panel element to a new parent, triggering connectedCallback and re-running setup. Fixed with an _initialized guard on both FileTreePanel and HtmlEditorPanel. For HtmlEditorPanel the iframe reloads from stale srcdoc on re-attach, losing typed content; fixed by setting _needsRestore = true in disconnectedCallback so the next onload re-renders from the top of the undo stack instead of running normal setup.

_initialized guard _needsRestore disconnectedCallback

Localization System

A localeGenerator.ts walks locales/en/ on server startup and generates src/locales/Locales.ts — a file of nested static classes mirroring the directory structure. File names become members with dots replaced by underscores and dashes converted to camelCase. A LocaleManager on the frontend fetches locale strings from GET /api/locales/:locale/:path and caches them by key.

server/localeGenerator.ts src/locales/Locales.ts src/locales/LocaleManager.ts server/routes/locales.ts

Editor Panel Header Redesign

Panel headers now show an icon and the panel's dynamic name inside the div.tc-tab tab strip label, updated via a bubbling panel:label-change custom event that TabContainer listens for. FileTree shows 📁 dirname; HtmlEditor shows 📄 filename. The panel headers and toolbars were simplified to contain only action buttons, left-aligned.

panel:label-change tc-tab label ftp-header hep-toolbar

FileTree Sub-Directory Root

Each FileTreePanel instance now has a _rootPath that can be changed independently of other instances. Double-clicking a directory label drills into it; a [ .. ] entry at the top navigates up one level. The tree is fetched in full each time and the subtree at _rootPath is sliced out client-side. The dirname shown in the tab label updates on every refresh.

_rootPath dblclick to drill [ .. ] entry per-instance

HTML Editor: Init and Pin Buttons

Init inserts a Hello World HTML template (with a <page-content> root element) into the current document, pushing it onto the undo stack so it can be undone. Enabled only when a document is open. Pin is a toggle button on the left of the toolbar that prevents the onDocumentOpened listener from switching the editor to a different file when a file is clicked in the tree. Turns orange when active.

hep-init hep-pin _pinned flag

Window Resize Handling

A ResizeObserver on .es-workspace recalculates panel flex sizes proportionally when the window changes size. It only acts when panels have been explicitly sized by dragging (guarded by c.style.flex being non-empty), leaving the default CSS flex: 1 layout untouched. Both the main three panels and any inner section splits within a panel are covered.

ResizeObserver _redistributeFlex editor-shell.ts

Portrait Bar Moved into Top Header

The three portrait panel selector buttons ( ) were moved from a separate full-width bar below the header into the header itself on the right side. They are hidden via CSS (display:none) in landscape and shown as a flex group when the .portrait class is active. The back link lost its "Projects" text label, keeping only the arrow. The title gained flex: 1 to push the buttons to the right edge.

.es-portrait-btns es-header portrait CSS class

Layout Persistence — Per User Per Device

Each browser gets a stable random device ID stored in localStorage under the key roject:deviceId. On load, EditorShell fetches the saved layout from GET /api/layout?deviceId=… and applies panel flex values and the last active portrait panel. After any panel resize or portrait switch, a debounced (800 ms) save fires to PUT /api/layout. Layout files are stored server-side at storage/layouts/{userId}/{deviceId}.json, making them per-user and per-device without any client-side cookie dependency.

server/routes/layout.ts storage/layouts/ localStorage deviceId debounced save

Key Decisions

EventSlot over string-based DOM events for all inter-component communication

String event names silently fail when mistyped. EventSlot is typed, directly coupled, and makes all listeners explicit and findable. No intermediate bus, no global event name registry.

panel:label-change as a bubbling DOM event (not EventSlot)

Panel-to-container communication crosses a DOM boundary that EventSlot cannot bridge directly (the container doesn't hold a reference to the panel at construction time). A bubbling CustomEvent is the right mechanism here — the container listens once and matches the event target against its tabs array.

FileTree root is per-instance, not global editor state

Two FileTree panels in two different tab containers should be able to show different subdirectories simultaneously. Storing _rootPath on the element instance (not on Editor) achieves this with no coordination overhead.

Layout stored server-side, device ID stored client-side

Storing layout in localStorage alone would lose it when the browser data is cleared and would not survive a device change. Storing it server-side keyed by a stable device ID gives persistence across browser resets while keeping it per-device as requested.

Direct writeFileSync in session store (no atomic rename)

session-file-store uses a write-to-temp-then-rename strategy that fails on Windows with EPERM when the target file is held open. Direct writeFileSync avoids the rename entirely at the cost of non-atomic writes, which is acceptable for session data.

Structural Changes

src/editor/Editor.ts — central singleton (moved from src/components/EditorState.ts)
server/sessionStore.ts — custom JsonSessionStore, replaces session-file-store
server/routes/layout.ts — new: GET/PUT layout per user per device
server/localeGenerator.ts — new: generates src/locales/Locales.ts on startup
server/routes/locales.ts — new: serves locale files
src/locales/LocaleManager.ts — new: frontend locale fetcher with cache
src/components/context-menu/ — new: ContextMenu class hierarchy
storage/layouts/ — new: per-user per-device layout JSON files
storage/sessions/ — session files (now written by custom store)