Friday, 24 July 2026

Session History

Page editor panel: structured .page format, block registry, rich-text toolbar, default theme.

What we built

Rename: html-editor-panelpage-editor-panel

The old HTML editor panel is replaced by page-editor-panel (PageEditorPanel), handling .page files instead of .html/.htm. All references updated across: FileEditorRegistry.ts, editor.html, editor-shell.ts, tab-container.ts, file-tree-panel.ts. The old folder was deleted. editor-shell no longer auto-opens index.html on startup — the panel shows an empty state until the user opens a .page file from the file tree.

Structured .page format

A .page file is a full HTML document whose <body> must contain exactly one <page-header>, one <page-root>, and one <page-footer> as direct children, in that order. <page-root> holds any number of <page-block> elements; each block contains one or more <page-area> elements for rich-text content.

Format validation is a placeholder function (validatePageFormat) that always returns true, documented for future implementation. When real validation is added, invalid files fall back to code-panel.

Auto-template for empty files: when a .page file is opened with empty content, validation is bypassed and the standard template (full-width block, default theme) is injected automatically. The document is marked dirty — the user must save to persist the structure.

Block registry

Available block templates are defined in a static table (PAGE_BLOCK_REGISTRY) in page-editor-panel.ts. Each entry has a name, optional CSS-sketch preview markup (using .pbp-* helper classes), and an html snippet appended to <page-root> when selected. Two standard blocks ship:

  • Full Width — one <page-area> spanning the full container width (pep-block-full).
  • Two Columns — two equal <page-area> elements side by side on landscape; stacked top-to-bottom on portrait via a CSS media query (pep-block-two-col).

Block items without a preview entry show their name as a text label inside the preview box.

Two-mode sidebar

Two icon buttons on the left edge of the panel switch between editing modes:

  • Blocks mode (⊞) — shows a horizontal scrollable list of block templates. Each entry has a CSS layout sketch preview above its name. Clicking a block appends it to <page-root> in the iframe and makes its new <page-area> elements contenteditable immediately, without a full re-render.
  • Areas mode (T) — shows the rich-text formatting toolbar. Buttons: B (bold), I (italic), U (underline), then a separator, then H1, H2, H3.

Rich-text: wrapSelection

Formatting is applied via wrapSelection(doc, range, tagName, attributes?) — no execCommand. The helper uses Range.extractContents() to pull the selected fragment out of the DOM, wraps it in the target element (created in the iframe document), and re-inserts via Range.insertNode(). The Range API automatically splits text nodes and element boundaries, so both fully-contained nodes and boundary intersections are handled correctly. Semantic tags are preferred: <b>, <i>, <u>, <h1><h3>. Adjacent identical elements are not merged after wrapping (future work, documented).

iframe sandbox + editor CSS injection

The editor iframe carries sandbox="allow-same-origin", blocking script execution for user-authored <script> tags. The sandbox attribute is documented as the single point to change if sandboxing needs adjustment.

Editor-side layout styles (PEP_EDITOR_STYLES) are injected into the live iframe <head> post-load as <style id="pep-editor-injected">. Before saving, the element is temporarily removed, outerHTML is captured, then the element is re-appended — so the injected styles never reach disk. The MutationObserver watches only <page-root>, so head mutations do not trigger change events.

Default theme: default-roject

All theme CSS rules are scoped to [data-theme="default-roject"]. The <body> and <page-root> in the standard template both carry this attribute so the theme applies to the full document and to all content inside blocks.

The theme CSS (DEFAULT_THEME_CSS) is embedded as a <style> block in the page <head> for new files. Future plan: replace with a <link> to styles.rokojori.com when themes are hosted there.

Styles defined:

  • Base: dark background (#0f1117), whitish copy (#c8cce0), Barlow font via @import from styles.rokojori.com, 1rem / 1.7 line-height.
  • h1: blue (#7c8cff), 2.5rem, 900 italic, uppercase.
  • h2: blue (#7c8cff), 1.6rem, 700.
  • h3: muted blue (#9ba4c7), 1.2rem, 700.
  • b / strong: bright white (#e2e4ed).

Tab container updates

Split submenu + vertical split

The single Split menu entry is replaced by a Split > submenu with ↔ Horizontally (adds a new section side by side in .es-sections) and ↕ Vertically (adds a second tab-container + horizontal resize handle inside the same .es-section). The tab-container:split event now carries a direction field. .es-section was already flex-direction: column, so no CSS changes were needed for vertical split.

Close Container

A Close Container context menu entry fires tab-container:close-container, handled by editor-shell: if the container is in a vertical split (multiple tab-container elements in the section), only that container and its adjacent handle are removed; otherwise the whole section and its adjacent es-v-handle are removed. The entry is hidden when the container is the last one in its .es-panel slot. If any tab has unsaved changes, showConfirmDialog prompts with Don't Close / Close Without Saving before dispatching the event.

Middle-mouse tab close

Tabs now close on middle-mouse click: a mousedown listener with e.button === 1 calls e.preventDefault() (suppresses the browser scroll cursor) and extracts the tab immediately.

EditorPanel / FileEditorPanel interface system

New file source/editor/editor-panel.ts defines the panel contract. EditorPanel requires __interfaces__: string[] and addContextMenuEntries(). FileEditorPanel extends EditorPanel adds hasUnsavedChanges(): boolean, replacing the old TabEntry.dirty flag — renderBar() now queries the panel live instead of caching a boolean. Each interface has a companion Definition class with static readonly type; implementsInterface(el, Def) is the single runtime check function. All five panels (page-editor-panel, code-panel, file-tree-panel, rojo-settings-panel, rojo-chat-panel) updated with __interfaces__.

Bug fixes

Section resize: setupResizeHandler previously observed only the workspace element, so section redistribution never fired when a panel was dragged (workspace size doesn't change on panel drag). Fixed by observing each .es-sections element directly — when the panel narrows, its sections container narrows too and the observer redistributes sections correctly.

Portrait → landscape: showPortraitPanel set inline display: none on panels and handles, but returning to landscape only removed the portrait class — the inline styles persisted and kept panels hidden. Fixed by clearing style.display on all panels and .es-v-handle elements when apply(false) runs.

Key decisions

Placeholder validation instead of gating immediately. The real format check (exactly one page-header / page-root / page-footer in body, no other elements) is documented but not enforced yet — validatePageFormat always returns true. This keeps the editor usable while the format stabilises, without hiding the validation contract.

wrapSelection for headings, not block-level replacement. H1/H2/H3 buttons wrap the selection the same way as B/I/U rather than replacing the parent block element. This is consistent with the existing system and avoids the complexity of block-level conversion for now. The limitation is documented.

Theme attribute on both <body> and <page-root>. Putting data-theme on <body> lets the base styles (background, font, colour) apply to the full document without a separate unscoped body { } rule. <page-root> keeps its own attribute for future per-block theme overrides.