2026-07-31 11:17:40 +00:00
|
|
|
|
<!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>
|
2026-07-31 13:43:04 +00:00
|
|
|
|
<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>
|
2026-07-31 11:17:40 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-07-31 13:43:04 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-07-31 11:17:40 +00:00
|
|
|
|
<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 >") 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>
|
|
|
|
|
|
|
2026-07-31 13:43:04 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-07-31 11:17:40 +00:00
|
|
|
|
</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>
|