Friday, 31 July 2026
File tree UX improvements, page editor full redesign (unified toolbar, floating block menu, insertion triggers, drag-to-reorder), InputDialog component, EditorConsole, console-panel tab.
Before this fix, calling refresh() on file-tree-panel
collapsed all open directories. The fix records which ftp-dir elements
carry the open class (via data-path on their
ftp-dir-label) before rebuilding the HTML, then re-adds
open to the matching labels after the new tree renders.
The double-click listener on ftp-dir-label was removed.
"As Root Directory" is now a context menu entry that appears when right-clicking
a directory, inside the existing showItemMenu() method.
isDir detection uses targetPath.endsWith('/').
When right-clicking a file that has a known editor, the context menu shows an
Open > submenu listing the default editor plus all registered
alternatives. Selecting an entry calls _openFileIn(path, editorTag),
which focuses an existing tab of that type or creates a new one.
Two static maps drive this:
_panelTypeMap (editorTag → panelType + label) and
_editorAlternatives (defaultEditorTag → alternativeEditorTags[]).
_openFileDefault (single click) focuses the file in any open editor
regardless of type; _openFileIn (context menu) focuses only in the
specified editor type.
The context menu title now shows only the filename (not the full path), truncated
to a configurable menuLabelMaxChars (20). Names longer than the limit
are shown as ...last-20-chars.
The left sidebar (.pep-sidebar) and its wrapper (.pep-main)
were removed. The Blocks (⊞) and Areas (T) mode buttons were moved directly into
.pep-toolbar, separated from the left-side toolbar items by a
.pep-toolbar-sep spacer (flex: 1) that pushes them to
the right. page-editor-panel now uses flex-direction: column
with three direct children: toolbar, mode panel, iframe.
New singleton EditorConsole (source/editor/EditorConsole.ts)
holds a capped ring of 500 ConsoleMessage objects
(text, type: 'info'|'error'|'hint',
timestamp) and dispatches them via onMessage: EventSlot.
showHover / hideHover dispatch a secondary
onHover: EventSlot<string | null> for future tooltip use.
editor-shell subscribes to onMessage and shows incoming
messages in a new .es-info 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. Editor.get().onFileTypeUnknown is now handled here,
logging to EditorConsole rather than as an inline error in
file-tree-panel.
console-panel is a new tab that renders all messages from
EditorConsole. Custom elements: conp-header,
conp-title, conp-list, conp-entry,
conp-time, conp-text. On connect it pre-populates
from EditorConsole.get().messages, then appends new entries as they
arrive. Time format: HH:MM:SS. Entry classes (conp-entry-error,
conp-entry-hint) colour conp-text accordingly.
Implements EditorPanelDefinition.type via __interfaces__.
The two-mode system (Blocks / Areas toggle) was removed. All formatting and block
actions now live in a single toolbar row:
[ BLOCK ] | [ H1 ] [ H2 ] [ H3 ] | [ LINK ] [ MARK ] | [ BOLD ] [ ITALIC ] [ UNDER ].
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
source/components/page-editor-panel/icons/.
mousedown → preventDefault on all format buttons preserves the iframe
contenteditable selection when clicking the toolbar.
Clicking the BLOCK button opens a floating overlay (.pep-block-menu)
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.
Thin .pep-insert-trigger overlays (10 px click area, 5 px visible via
::before) are injected before every page-block and after
the last one. Interaction is split on mousedown:
.pep-drag-bar) appears in the outer component and follows the cursor,
snapping to the nearest trigger's Y position. The nearest trigger gets
.pep-drop-target (brighter highlight). On mouseup the
dragged block is moved to the drop position. No-op guards prevent moving a block
before or immediately after itself.
The end trigger is click-only — no block follows it, so drag is a no-op there.
A fallback mouseup on the outer document handles release
outside the iframe.
A .pep-delete-btn (✕) is injected top-right inside every
page-block. On landscape (@media (hover: hover)) it appears
on block hover via CSS. On portrait, tapping anywhere on a block (no contenteditable
required) adds .pep-block-active via event delegation on
page-root; 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.
Two new toolbar actions: LINK wraps the selection in
<a href="..."> after prompting for a URL (selected text is used
as the default if it starts with https?://).
MARK wraps in <marked-text>.
CSS for both is injected into the iframe and saved in the page file:
marked-text — bold, hsl(190, 80%, 90%);
a — hsl(200, 80%, 70%).
New InputDialog class added to confirm-dialog.ts, following
the same visual pattern as ConfirmDialog (backdrop, titlebar with icon /
title / ✕, body, footer). Body contains a label and a full-width text input.
Enter submits, Escape cancels. Returns Promise<string | null>.
Exported as showInputDialog(options). The
confirm-dialog, input-dialog CSS selector gives both dialogs
position: fixed; inset: 0 centering.
openDocumentIn ignored the target panel type
Editor.openDocumentIn(filePath, panelElement) was re-resolving
editorTag from the FileEditorRegistry, which always
returned the default editor for the file type (e.g. page-editor-panel
for .page files). The code-panel listener checked
if ('code-panel' !== e.editorTag) return and bailed, so the file
appeared to open but showed no content.
Fix: derive editorTag from panelElement.tagName.toLowerCase()
directly, removing the registry lookup entirely from openDocumentIn.
The caller already decided which panel to target; the dispatch should respect that
decision.
EditorConsole as a standalone singleton, not part of
Editor.
Keeping it separate means any component (including future non-editor panels) can
import just EditorConsole without pulling in the full editor state.
Editor.onFileTypeUnknown is bridged to it in editor-shell,
the single component that knows about both.
Two open-file methods instead of one.
_openFileDefault (single click) focuses the file in any editor;
_openFileIn (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.
document.cloneNode(true) for HTML capture.
Previously, _captureHtml() 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.
Manual _onContentChanged() before _refreshIframeOverlays()
in insert and delete.
Calling MutationObserver.disconnect() inside _refreshIframeOverlays
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
_onContentChanged() synchronously first, then refresh overlays.
Event delegation on page-root for portrait block-tap.
Attaching per-block click listeners in _refreshIframeOverlays would stack
duplicate listeners across repeated refresh calls (blocks are not replaced, only
triggers and delete buttons are). A single delegated listener on page-root
— removed and re-added each refresh — avoids the accumulation.