348 lines
16 KiB
HTML
348 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Session Summary — 5 July 2026</title>
|
|
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
|
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
|
|
<header>
|
|
<p class="date">Sunday, 5 July 2026</p>
|
|
<h1>Roject — Session Summary</h1>
|
|
<p class="subtitle">
|
|
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.
|
|
</p>
|
|
</header>
|
|
|
|
<section>
|
|
<h2>What we built</h2>
|
|
|
|
<div class="card">
|
|
<h3>ContextMenu Class Hierarchy</h3>
|
|
<p>
|
|
Introduced a plain class hierarchy (not a Web Component) for building
|
|
nested context menus: <code>ContextMenuDirectory</code>,
|
|
<code>ContextMenuEntry</code>, <code>ContextMenuReadOnlyEntry</code>,
|
|
and <code>ContextMenuSeparator</code>. The root directory has
|
|
<code>show(x, y)</code> which renders lazily and positions itself in the
|
|
correct viewport corner (right/left, below/above) based on available space.
|
|
A <code>clearCloseUpwards()</code> 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.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">src/components/context-menu/</span>
|
|
<span class="tag">smart corner positioning</span>
|
|
<span class="tag">clearCloseUpwards</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Tab Container ⋮ Menu</h3>
|
|
<p>
|
|
Replaced the split <code>⊟</code> button with a <code>⋮</code> menu button
|
|
that opens a ContextMenu. Options: <em>Add ></em> (HTML Editor, File Tree),
|
|
<em>Duplicate</em>, <em>Split</em>, <em>Close</em>. Duplicate was fixed by
|
|
storing a <code>factory: () => HTMLElement</code> function in each
|
|
<code>TabEntry</code> so new instances can be created without re-using the
|
|
existing element. The <code>tab-container:add-panel</code> event is now
|
|
handled in EditorShell's <code>setupSplitListener</code>.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">tab-container.ts</span>
|
|
<span class="tag">TabEntry.factory</span>
|
|
<span class="tag">tab-container:add-panel</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>FileTree: Add File / Add Directory</h3>
|
|
<p>
|
|
Added <code>+F</code> and <code>+D</code> 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
|
|
<code>POST /:projectId/create-file</code> and
|
|
<code>POST /:projectId/create-directory</code> were added to
|
|
<code>server/routes/files.ts</code>. On success the component dispatches
|
|
<code>Editor.onFilesChanged</code> so all FileTree instances refresh.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">file-tree-panel.ts</span>
|
|
<span class="tag">server/routes/files.ts</span>
|
|
<span class="tag">create-file / create-directory</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>EventSlot Refactor</h3>
|
|
<p>
|
|
Replaced all string-based <code>document.dispatchEvent</code> /
|
|
<code>addEventListener</code> calls with typed
|
|
<code>EventSlot<T></code> instances on the <code>Editor</code>
|
|
singleton. Slots: <code>onDocumentOpened</code>,
|
|
<code>onDocumentDirty</code>, <code>onDocumentSaved</code>,
|
|
<code>onFilesChanged</code>. Components call
|
|
<code>Editor.get().onDocumentOpened.addListener(…)</code> directly —
|
|
no string keys, no silent mismatches.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">EventSlot<T></span>
|
|
<span class="tag">src/editor/Editor.ts</span>
|
|
<span class="tag">hard coupling</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Session Persistence</h3>
|
|
<p>
|
|
<code>session-file-store</code> was removed after it caused an
|
|
<code>EPERM: operation not permitted, rename</code> error on Windows
|
|
(atomic rename not permitted on the session file). Replaced with a custom
|
|
<code>JsonSessionStore extends session.Store</code> that writes directly
|
|
with <code>fs.writeFileSync</code> — no temporary file, no rename.
|
|
Sessions are stored per-ID in <code>storage/sessions/</code>.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">server/sessionStore.ts</span>
|
|
<span class="tag">JsonSessionStore</span>
|
|
<span class="tag">Windows EPERM fix</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Tab Drag State Preservation</h3>
|
|
<p>
|
|
Dragging a tab between containers re-attaches the panel element to a new
|
|
parent, triggering <code>connectedCallback</code> and re-running setup.
|
|
Fixed with an <code>_initialized</code> guard on both FileTreePanel and
|
|
HtmlEditorPanel. For HtmlEditorPanel the iframe reloads from stale
|
|
<code>srcdoc</code> on re-attach, losing typed content; fixed by setting
|
|
<code>_needsRestore = true</code> in <code>disconnectedCallback</code>
|
|
so the next <code>onload</code> re-renders from the top of the undo stack
|
|
instead of running normal setup.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">_initialized guard</span>
|
|
<span class="tag">_needsRestore</span>
|
|
<span class="tag">disconnectedCallback</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Localization System</h3>
|
|
<p>
|
|
A <code>localeGenerator.ts</code> walks <code>locales/en/</code> on server
|
|
startup and generates <code>src/locales/Locales.ts</code> — 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 <code>LocaleManager</code> on the frontend fetches locale strings from
|
|
<code>GET /api/locales/:locale/:path</code> and caches them by key.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">server/localeGenerator.ts</span>
|
|
<span class="tag">src/locales/Locales.ts</span>
|
|
<span class="tag">src/locales/LocaleManager.ts</span>
|
|
<span class="tag">server/routes/locales.ts</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Editor Panel Header Redesign</h3>
|
|
<p>
|
|
Panel headers now show an icon and the panel's dynamic name inside the
|
|
<code>div.tc-tab</code> tab strip label, updated via a bubbling
|
|
<code>panel:label-change</code> custom event that TabContainer listens for.
|
|
FileTree shows <code>📁 dirname</code>; HtmlEditor shows
|
|
<code>📄 filename</code>. The panel headers and toolbars were simplified
|
|
to contain only action buttons, left-aligned.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">panel:label-change</span>
|
|
<span class="tag">tc-tab label</span>
|
|
<span class="tag">ftp-header</span>
|
|
<span class="tag">hep-toolbar</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>FileTree Sub-Directory Root</h3>
|
|
<p>
|
|
Each FileTreePanel instance now has a <code>_rootPath</code> that can be
|
|
changed independently of other instances. Double-clicking a directory label
|
|
drills into it; a <code>[ .. ]</code> entry at the top navigates up one
|
|
level. The tree is fetched in full each time and the subtree at
|
|
<code>_rootPath</code> is sliced out client-side. The dirname shown in the
|
|
tab label updates on every refresh.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">_rootPath</span>
|
|
<span class="tag">dblclick to drill</span>
|
|
<span class="tag">[ .. ] entry</span>
|
|
<span class="tag">per-instance</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>HTML Editor: Init and Pin Buttons</h3>
|
|
<p>
|
|
<strong>Init</strong> inserts a Hello World HTML template (with a
|
|
<code><page-content></code> root element) into the current document,
|
|
pushing it onto the undo stack so it can be undone. Enabled only when a
|
|
document is open. <strong>Pin</strong> is a toggle button on the left of the
|
|
toolbar that prevents the <code>onDocumentOpened</code> listener from
|
|
switching the editor to a different file when a file is clicked in the tree.
|
|
Turns orange when active.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">hep-init</span>
|
|
<span class="tag">hep-pin</span>
|
|
<span class="tag">_pinned flag</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Window Resize Handling</h3>
|
|
<p>
|
|
A <code>ResizeObserver</code> on <code>.es-workspace</code> recalculates
|
|
panel flex sizes proportionally when the window changes size. It only acts
|
|
when panels have been explicitly sized by dragging (guarded by
|
|
<code>c.style.flex</code> being non-empty), leaving the default CSS
|
|
<code>flex: 1</code> layout untouched. Both the main three panels and any
|
|
inner section splits within a panel are covered.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">ResizeObserver</span>
|
|
<span class="tag">_redistributeFlex</span>
|
|
<span class="tag">editor-shell.ts</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Portrait Bar Moved into Top Header</h3>
|
|
<p>
|
|
The three portrait panel selector buttons (<code>⊟</code> <code>⊡</code>
|
|
<code>⊞</code>) were moved from a separate full-width bar below the header
|
|
into the header itself on the right side. They are hidden via CSS
|
|
(<code>display:none</code>) in landscape and shown as a flex group when the
|
|
<code>.portrait</code> class is active. The back link lost its "Projects"
|
|
text label, keeping only the <code>←</code> arrow. The title gained
|
|
<code>flex: 1</code> to push the buttons to the right edge.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">.es-portrait-btns</span>
|
|
<span class="tag">es-header</span>
|
|
<span class="tag">portrait CSS class</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Layout Persistence — Per User Per Device</h3>
|
|
<p>
|
|
Each browser gets a stable random device ID stored in
|
|
<code>localStorage</code> under the key <code>roject:deviceId</code>.
|
|
On load, EditorShell fetches the saved layout from
|
|
<code>GET /api/layout?deviceId=…</code> 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 <code>PUT /api/layout</code>. Layout
|
|
files are stored server-side at
|
|
<code>storage/layouts/{userId}/{deviceId}.json</code>, making them
|
|
per-user and per-device without any client-side cookie dependency.
|
|
</p>
|
|
<div class="tags">
|
|
<span class="tag">server/routes/layout.ts</span>
|
|
<span class="tag">storage/layouts/</span>
|
|
<span class="tag">localStorage deviceId</span>
|
|
<span class="tag">debounced save</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Key Decisions</h2>
|
|
|
|
<div class="decision">
|
|
<strong>EventSlot over string-based DOM events for all inter-component communication</strong>
|
|
<p>
|
|
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.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>panel:label-change as a bubbling DOM event (not EventSlot)</strong>
|
|
<p>
|
|
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
|
|
<code>tabs</code> array.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>FileTree root is per-instance, not global editor state</strong>
|
|
<p>
|
|
Two FileTree panels in two different tab containers should be able to show
|
|
different subdirectories simultaneously. Storing <code>_rootPath</code> on
|
|
the element instance (not on <code>Editor</code>) achieves this with no
|
|
coordination overhead.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>Layout stored server-side, device ID stored client-side</strong>
|
|
<p>
|
|
Storing layout in <code>localStorage</code> 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.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="decision">
|
|
<strong>Direct writeFileSync in session store (no atomic rename)</strong>
|
|
<p>
|
|
<code>session-file-store</code> uses a write-to-temp-then-rename strategy
|
|
that fails on Windows with EPERM when the target file is held open. Direct
|
|
<code>writeFileSync</code> avoids the rename entirely at the cost of
|
|
non-atomic writes, which is acceptable for session data.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Structural Changes</h2>
|
|
<div class="card">
|
|
<p>
|
|
<code>src/editor/Editor.ts</code> — central singleton (moved from <code>src/components/EditorState.ts</code>)<br>
|
|
<code>server/sessionStore.ts</code> — custom <code>JsonSessionStore</code>, replaces <code>session-file-store</code><br>
|
|
<code>server/routes/layout.ts</code> — new: GET/PUT layout per user per device<br>
|
|
<code>server/localeGenerator.ts</code> — new: generates <code>src/locales/Locales.ts</code> on startup<br>
|
|
<code>server/routes/locales.ts</code> — new: serves locale files<br>
|
|
<code>src/locales/LocaleManager.ts</code> — new: frontend locale fetcher with cache<br>
|
|
<code>src/components/context-menu/</code> — new: ContextMenu class hierarchy<br>
|
|
<code>storage/layouts/</code> — new: per-user per-device layout JSON files<br>
|
|
<code>storage/sessions/</code> — session files (now written by custom store)
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<footer>
|
|
Roject — session log — 5 July 2026
|
|
</footer>
|
|
|
|
</div>
|
|
<script>var NAV_ROOT = '../../../../';</script>
|
|
<script src="../../../../_assets_/nav-data.js"></script>
|
|
<script src="../../../../_assets_/nav.js"></script>
|
|
</body>
|
|
</html>
|