Sunday, 5 July 2026
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.
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.
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.
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.
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.
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/.
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.
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.
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.
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.
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.
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.
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.
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.
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-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.
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.
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.
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.
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)