rojects/workspace/history/2026/07-July/05-Sunday/index.html

348 lines
16 KiB
HTML
Raw Permalink Normal View History

<!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 &gt;</em> (HTML Editor, File Tree),
<em>Duplicate</em>, <em>Split</em>, <em>Close</em>. Duplicate was fixed by
storing a <code>factory: () =&gt; 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&lt;T&gt;</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&lt;T&gt;</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>&lt;page-content&gt;</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 &mdash; session log &mdash; 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>