347 lines
17 KiB
HTML
347 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Friday, 31 July 2026 — Roject</title>
|
||
<link rel="stylesheet" href="../../../../_assets_/styles.css">
|
||
<link rel="stylesheet" href="../../../../_assets_/nav.css">
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
|
||
<header>
|
||
<p class="date">Friday, 31 July 2026</p>
|
||
<h1>Session History</h1>
|
||
<p class="subtitle">File tree UX improvements, page editor full redesign (unified toolbar, floating block menu, insertion triggers, drag-to-reorder), InputDialog component, EditorConsole, console-panel tab.</p>
|
||
</header>
|
||
|
||
<section>
|
||
<h2>What we built</h2>
|
||
|
||
<div class="card">
|
||
<h3>File tree: open-state preservation on refresh</h3>
|
||
<p>
|
||
Before this fix, calling <code>refresh()</code> on <code>file-tree-panel</code>
|
||
collapsed all open directories. The fix records which <code>ftp-dir</code> elements
|
||
carry the <code>open</code> class (via <code>data-path</code> on their
|
||
<code>ftp-dir-label</code>) before rebuilding the HTML, then re-adds
|
||
<code>open</code> to the matching labels after the new tree renders.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>File tree: "Mark As Root Directory" moved to context menu</h3>
|
||
<p>
|
||
The double-click listener on <code>ftp-dir-label</code> was removed.
|
||
"As Root Directory" is now a context menu entry that appears when right-clicking
|
||
a directory, inside the existing <code>showItemMenu()</code> method.
|
||
<code>isDir</code> detection uses <code>targetPath.endsWith('/')</code>.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>File tree: "Open >" submenu for alternate editors</h3>
|
||
<p>
|
||
When right-clicking a file that has a known editor, the context menu shows an
|
||
<em>Open ></em> submenu listing the default editor plus all registered
|
||
alternatives. Selecting an entry calls <code>_openFileIn(path, editorTag)</code>,
|
||
which focuses an existing tab of that type or creates a new one.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
Two static maps drive this:
|
||
<code>_panelTypeMap</code> (editorTag → panelType + label) and
|
||
<code>_editorAlternatives</code> (defaultEditorTag → alternativeEditorTags[]).
|
||
<code>_openFileDefault</code> (single click) focuses the file in any open editor
|
||
regardless of type; <code>_openFileIn</code> (context menu) focuses only in the
|
||
specified editor type.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>File tree: context menu label truncation</h3>
|
||
<p>
|
||
The context menu title now shows only the filename (not the full path), truncated
|
||
to a configurable <code>menuLabelMaxChars</code> (20). Names longer than the limit
|
||
are shown as <code>...last-20-chars</code>.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: mode buttons moved into toolbar</h3>
|
||
<p>
|
||
The left sidebar (<code>.pep-sidebar</code>) and its wrapper (<code>.pep-main</code>)
|
||
were removed. The Blocks (⊞) and Areas (T) mode buttons were moved directly into
|
||
<code>.pep-toolbar</code>, separated from the left-side toolbar items by a
|
||
<code>.pep-toolbar-sep</code> spacer (<code>flex: 1</code>) that pushes them to
|
||
the right. <code>page-editor-panel</code> now uses <code>flex-direction: column</code>
|
||
with three direct children: toolbar, mode panel, iframe.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>EditorConsole — centralised message system</h3>
|
||
<p>
|
||
New singleton <code>EditorConsole</code> (<code>source/editor/EditorConsole.ts</code>)
|
||
holds a capped ring of 500 <code>ConsoleMessage</code> objects
|
||
(<code>text</code>, <code>type</code>: <code>'info'|'error'|'hint'</code>,
|
||
<code>timestamp</code>) and dispatches them via <code>onMessage: EventSlot</code>.
|
||
<code>showHover</code> / <code>hideHover</code> dispatch a secondary
|
||
<code>onHover: EventSlot<string | null></code> for future tooltip use.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<code>editor-shell</code> subscribes to <code>onMessage</code> and shows incoming
|
||
messages in a new <code>.es-info</code> element in the header — fades in for 5 s
|
||
then fades out. Portrait mode hides the inline element and would show a fixed bottom
|
||
bar instead. <code>Editor.get().onFileTypeUnknown</code> is now handled here,
|
||
logging to <code>EditorConsole</code> rather than as an inline error in
|
||
<code>file-tree-panel</code>.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>console-panel — new editor tab</h3>
|
||
<p>
|
||
<code>console-panel</code> is a new tab that renders all messages from
|
||
<code>EditorConsole</code>. Custom elements: <code>conp-header</code>,
|
||
<code>conp-title</code>, <code>conp-list</code>, <code>conp-entry</code>,
|
||
<code>conp-time</code>, <code>conp-text</code>. On connect it pre-populates
|
||
from <code>EditorConsole.get().messages</code>, then appends new entries as they
|
||
arrive. Time format: <code>HH:MM:SS</code>. Entry classes (<code>conp-entry-error</code>,
|
||
<code>conp-entry-hint</code>) colour <code>conp-text</code> accordingly.
|
||
Implements <code>EditorPanelDefinition.type</code> via <code>__interfaces__</code>.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: unified toolbar with icon + label buttons</h3>
|
||
<p>
|
||
The two-mode system (Blocks / Areas toggle) was removed. All formatting and block
|
||
actions now live in a single toolbar row:
|
||
<code>[ BLOCK ] | [ H1 ] [ H2 ] [ H3 ] | [ LINK ] [ MARK ] | [ BOLD ] [ ITALIC ] [ UNDER ]</code>.
|
||
Each button shows a 24×24 SVG icon on top and a short uppercase label (max 6 characters,
|
||
~10 px) below. SVG placeholders are in
|
||
<code>source/components/page-editor-panel/icons/</code>.
|
||
<code>mousedown → preventDefault</code> on all format buttons preserves the iframe
|
||
contenteditable selection when clicking the toolbar.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: floating block menu</h3>
|
||
<p>
|
||
Clicking the BLOCK button opens a floating overlay (<code>.pep-block-menu</code>)
|
||
positioned absolutely within the component. Default placement: below the anchor;
|
||
flips above when too close to the bottom edge. An ✕ button closes the menu;
|
||
clicking outside also closes it. The menu is shown offscreen first to measure its
|
||
height before final positioning.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: block insertion triggers and drag-to-reorder</h3>
|
||
<p>
|
||
Thin <code>.pep-insert-trigger</code> overlays (10 px click area, 5 px visible via
|
||
<code>::before</code>) are injected before every <code>page-block</code> and after
|
||
the last one. Interaction is split on <code>mousedown</code>:
|
||
</p>
|
||
<ul style="margin-top:0.5rem;line-height:1.9">
|
||
<li><strong>Click (no movement)</strong> — opens the block menu to insert before
|
||
that block (or append for the end trigger).</li>
|
||
<li><strong>Drag (> 4 px movement)</strong> — a snapping bar
|
||
(<code>.pep-drag-bar</code>) appears in the outer component and follows the cursor,
|
||
snapping to the nearest trigger's Y position. The nearest trigger gets
|
||
<code>.pep-drop-target</code> (brighter highlight). On <code>mouseup</code> the
|
||
dragged block is moved to the drop position. No-op guards prevent moving a block
|
||
before or immediately after itself.</li>
|
||
</ul>
|
||
<p style="margin-top:0.75rem">
|
||
The end trigger is click-only — no block follows it, so drag is a no-op there.
|
||
A fallback <code>mouseup</code> on the outer <code>document</code> handles release
|
||
outside the iframe.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: block deletion</h3>
|
||
<p>
|
||
A <code>.pep-delete-btn</code> (✕) is injected top-right inside every
|
||
<code>page-block</code>. On landscape (<code>@media (hover: hover)</code>) it appears
|
||
on block hover via CSS. On portrait, tapping anywhere on a block (no contenteditable
|
||
required) adds <code>.pep-block-active</code> via event delegation on
|
||
<code>page-root</code>; all other blocks lose it. Tapping in another block or outside
|
||
all blocks (body listener) dismisses the active state. The delete button removes the
|
||
block, records an undo entry, then rebuilds overlays.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Page editor: LINK and MARK formats, styled custom elements</h3>
|
||
<p>
|
||
Two new toolbar actions: <strong>LINK</strong> wraps the selection in
|
||
<code><a href="..."></code> after prompting for a URL (selected text is used
|
||
as the default if it starts with <code>https?://</code>).
|
||
<strong>MARK</strong> wraps in <code><marked-text></code>.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
CSS for both is injected into the iframe and saved in the page file:
|
||
<code>marked-text</code> — bold, <code>hsl(190, 80%, 90%)</code>;
|
||
<code>a</code> — <code>hsl(200, 80%, 70%)</code>.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>InputDialog component</h3>
|
||
<p>
|
||
New <code>InputDialog</code> class added to <code>confirm-dialog.ts</code>, following
|
||
the same visual pattern as <code>ConfirmDialog</code> (backdrop, titlebar with icon /
|
||
title / ✕, body, footer). Body contains a label and a full-width text input.
|
||
Enter submits, Escape cancels. Returns <code>Promise<string | null></code>.
|
||
Exported as <code>showInputDialog(options)</code>. The
|
||
<code>confirm-dialog, input-dialog</code> CSS selector gives both dialogs
|
||
<code>position: fixed; inset: 0</code> centering.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Bug fix: <code>openDocumentIn</code> ignored the target panel type</h3>
|
||
<p>
|
||
<code>Editor.openDocumentIn(filePath, panelElement)</code> was re-resolving
|
||
<code>editorTag</code> from the <code>FileEditorRegistry</code>, which always
|
||
returned the <em>default</em> editor for the file type (e.g. <code>page-editor-panel</code>
|
||
for <code>.page</code> files). The <code>code-panel</code> listener checked
|
||
<code>if ('code-panel' !== e.editorTag) return</code> and bailed, so the file
|
||
appeared to open but showed no content.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
Fix: derive <code>editorTag</code> from <code>panelElement.tagName.toLowerCase()</code>
|
||
directly, removing the registry lookup entirely from <code>openDocumentIn</code>.
|
||
The caller already decided which panel to target; the dispatch should respect that
|
||
decision.
|
||
</p>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<div class="card">
|
||
<h3>Per-project per-device layout persistence</h3>
|
||
<p>
|
||
The editor now fully saves and restores its window configuration — tab structure,
|
||
open files, panel widths — per project per device.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong>Storage:</strong> <code>.roject/layout-<deviceId>.json</code> inside
|
||
each project's root directory. Remote server projects write to
|
||
<code>storage/<id>/root/.roject/</code>; local Electron projects write to
|
||
<code><localRoot>/.roject/</code>; remote projects opened via the Electron
|
||
proxy use the centralized <code>build/data/storage/layouts/</code> keyed by
|
||
device + remote project ID. <code>deviceId</code> is already a per-browser UUID
|
||
in <code>localStorage</code> — Firefox, Chrome, and Electron automatically get
|
||
distinct IDs.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong>Serialized format</strong> (<code>SerializedLayout</code>):
|
||
<code>{ version, activePortraitPanel, panels: { left, center, right } }</code>
|
||
where each panel carries its flex value and an array of sections; each section
|
||
carries its flex and an array of tab-containers; each tab-container carries
|
||
<code>activeTabId</code> and an array of tabs with
|
||
<code>{ id, label, panelType, tag, openFile }</code>.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong><code>FileEditorPanel</code> interface</strong> extended with
|
||
<code>getCurrentFile(): string | null</code>, implemented by
|
||
<code>code-panel</code> and <code>page-editor-panel</code> (both already tracked
|
||
<code>currentPath</code>).
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong>Restore flow:</strong> <code>editor-shell</code> awaits the layout fetch
|
||
alongside <code>customElements.whenDefined</code> promises; if a layout with
|
||
<code>panels</code> is returned, <code>_restoreLayout()</code> rebuilds the DOM
|
||
(sections, tab-containers with resize handles), calls <code>addTab()</code> once
|
||
the containers are connected, activates the correct tab, then opens each saved
|
||
file via <code>Editor.openDocumentIn()</code>. Falls back to
|
||
<code>initDefaultLayout()</code> if no layout exists.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong>Save triggers:</strong> panel resize, inner section/tab-container resize
|
||
(via <code>makeResizeHandle</code>'s new optional <code>onResize</code> callback),
|
||
tab click, file opened (<code>Editor.onDocumentOpened</code>), split,
|
||
close-container, add-panel, portrait panel switch. All debounced at 800 ms.
|
||
</p>
|
||
<p style="margin-top:0.75rem">
|
||
<strong><code>.roject/</code> hidden</strong> from both the remote
|
||
(<code>storage.ts buildTree</code>) and local (<code>localFiles.ts buildTree</code>)
|
||
file tree listings via a server-side <code>.filter(name !== '.roject')</code>.
|
||
</p>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<section>
|
||
<h2>Key decisions</h2>
|
||
|
||
<div class="card">
|
||
<p>
|
||
<strong><code>EditorConsole</code> as a standalone singleton, not part of
|
||
<code>Editor</code>.</strong>
|
||
Keeping it separate means any component (including future non-editor panels) can
|
||
import just <code>EditorConsole</code> without pulling in the full editor state.
|
||
<code>Editor.onFileTypeUnknown</code> is bridged to it in <code>editor-shell</code>,
|
||
the single component that knows about both.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<p>
|
||
<strong>Two open-file methods instead of one.</strong>
|
||
<code>_openFileDefault</code> (single click) focuses the file in any editor;
|
||
<code>_openFileIn</code> (context menu "Open >") focuses only in the specified
|
||
editor type and forces that type when creating a new tab. This makes the intent
|
||
explicit without adding a flag parameter.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<p>
|
||
<strong><code>document.cloneNode(true)</code> for HTML capture.</strong>
|
||
Previously, <code>_captureHtml()</code> temporarily removed injected overlay elements
|
||
from the live DOM, serialised, then re-added them — causing flickering and complexity.
|
||
Cloning the document first and stripping overlays from the clone leaves the live DOM
|
||
untouched entirely.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<p>
|
||
<strong>Manual <code>_onContentChanged()</code> before <code>_refreshIframeOverlays()</code>
|
||
in insert and delete.</strong>
|
||
Calling <code>MutationObserver.disconnect()</code> inside <code>_refreshIframeOverlays</code>
|
||
discards any pending (not yet delivered) mutation callbacks — meaning a block insert or
|
||
delete would not be recorded in the undo stack. The fix is to call
|
||
<code>_onContentChanged()</code> synchronously first, then refresh overlays.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<p>
|
||
<strong>Event delegation on <code>page-root</code> for portrait block-tap.</strong>
|
||
Attaching per-block click listeners in <code>_refreshIframeOverlays</code> would stack
|
||
duplicate listeners across repeated refresh calls (blocks are not replaced, only
|
||
triggers and delete buttons are). A single delegated listener on <code>page-root</code>
|
||
— removed and re-added each refresh — avoids the accumulation.
|
||
</p>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<footer>
|
||
Roject — session history
|
||
</footer>
|
||
|
||
</div>
|
||
<script>var NAV_ROOT = '../../../../';</script>
|
||
<script src="../../../../_assets_/nav-data.js"></script>
|
||
<script src="../../../../_assets_/nav.js"></script>
|
||
</body>
|
||
</html>
|