rojects/workspace/history/2026/07-July/31-Friday/index.html

293 lines
14 KiB
HTML
Raw 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>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 &gt;" submenu for alternate editors</h3>
<p>
When right-clicking a file that has a known editor, the context menu shows an
<em>Open &gt;</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&lt;string | null&gt;</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 (&gt; 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>&lt;a href="..."&gt;</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>&lt;marked-text&gt;</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&lt;string | null&gt;</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>
<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 &gt;") 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 &mdash; session history
</footer>
</div>
<script>var NAV_ROOT = '../../../../';</script>
<script src="../../../../_assets_/nav-data.js"></script>
<script src="../../../../_assets_/nav.js"></script>
</body>
</html>