Skip to content

Slash menu, markdown input rules, canonical copy/cut and type-to-replace#15

Merged
namedgraph merged 5 commits into
masterfrom
feature/editor-quick-wins
Jul 12, 2026
Merged

Slash menu, markdown input rules, canonical copy/cut and type-to-replace#15
namedgraph merged 5 commits into
masterfrom
feature/editor-quick-wins

Conversation

@namedgraph

Copy link
Copy Markdown
Member

Summary

Three editor-feel features. The slash menu and markdown rules are a manual port of the corresponding halves of #10 (notion-affordances) onto the content-model era — that branch predates both the M2 state container and #13, so a rebase was not viable; its design survives (priority-raised beforeinput trigger dispatcher with next-match into undo's coalescer, show-at-point positioning split, the slash menu as keyboard framing over existing toolbar actions), its code was retargeted at today's primitives. The @-mention and block-handle menu halves of #10 are not included — they can be ported later onto this same foundation, so this does not close #10.

  • Slash menu (src/input.xsl): / in an empty host opens a filterable, keyboard-navigable menu showing only the commands the caret context admits — conversions for text-block hosts, Quote where the wrap is legal, lists where the model places one, Figure…/Table… routed to the existing dialogs. An empty list item offers just Figure/Table; an empty cell paragraph offers everything.
  • Markdown input rules: # /## /### , - /* , 1. , ```, and > act on the host (top-level p, quote-p, cell-p) and are content-model-gated — a shorthand whose result the model rejects stays literal. `> ` wraps (`blockquote > p`) rather than converting, matching the storage contract. One undo restores the literal marker.
  • Canonical copy/cut (src/select.xsl): a cross-host selection is copied in its storage form — cloneContents through the canonical + cm-normalize pipeline — so the clipboard HTML has no chrome/contenteditable/marker classes but keeps RDFa attributes; annotated content round-trips between documents. Cut adds the delete machine (one region-keyed undo entry); within-host copy stays native.
  • Type-to-replace: a printable character over a cross-host selection deletes it and lands at the machine-placed caret (seam included), from host focus and the page background alike — one gesture, one undo entry. Enter/Tab/paste stay suppressed.
  • Fixes a latent convert-block bug (element-level caret on the converted block dangled after replaceWithAdd @-mention and Notion-style UX quick wins #10's fix, which empty-block slash/markdown conversions would have tripped constantly).

Test plan

  • New tests/browser/notion.mjs (31 assertions): open/filter/keyboard-pick/click paths, context filtering in list items and flow cells, figure routing to the dialog, all five markdown rules incl. nested-host conversion and literal guards, undo granularity, I1–I5 invariants after every mutation.
  • tests/browser/select.mjs extended: type-to-replace (stage 2 and sweep-with-seam), canonical copy (no editing ephemera, RDFa kept, plain-text flavor), native within-host copy untouched, cut with merge + exact one-undo restore.
  • Full matrix green: 13 browser suites, zero failed assertions; 47/47 headless (no extractor/canonical changes).

🤖 Generated with Claude Code

namedgraph and others added 5 commits July 12, 2026 08:46
Ported from the notion-affordances branch onto the content-model era: one
priority-raised ixsl:onbeforeinput dispatcher (src/input.xsl) intercepts
printable-char triggers and next-matches into undo's typing coalescer, so
plain typing still snapshots. Triggers act on the HOST the caret sits in
and every conversion is content-model-gated - a shorthand whose result
the model rejects stays literal text.

- / in an empty host opens a filterable, keyboard-navigable menu showing
  only the commands the context admits (conversions for text-block hosts,
  Quote where the wrap is legal, lists where the model places one,
  figure/table always - routed to the existing dialogs); menu popups
  position via show-at-caret over the refactored show-at-point.
- Markdown shorthands in a paragraph host: #/##/### + space, -/* +
  space, 1. + space, ``` -> convert in place; '> ' WRAPS in a blockquote
  (blockquote > p - converting the p itself would be invalid). Undo
  restores the literal marker in one step.
- convert-block: an element-level caret on the block itself dangled
  after replaceWith (PR #10's fix) - restore only nodes that move.

New notion.mjs suite (31 assertions incl. context filtering in list
items and flow cells, nested-host conversion, I1-I5 invariants).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy of a selection spanning hosts now puts the STORAGE form on the
clipboard instead of the editing DOM: cloneContents of the region-clamped
range runs the canonical + cm-normalize pipeline (paste in reverse), so
the HTML flavor carries no chrome, contenteditable or marker classes but
keeps RDFa attributes - annotated content round-trips between documents.
Cut adds the delete machine (one region-keyed undo entry). Within-host
copy stays native. The clamp steps are factored into local:clamped-range,
shared by the delete machine and copy.

Typing a printable character over a cross-host selection now replaces it:
the delete machine collapses the caret into a host and the character
lands there (local:insert-text-at-caret) - one gesture, one undo entry,
from host focus and from the page background alike. Enter/Tab/paste stay
suppressed.

select.mjs: type-to-replace (stage 2 and sweep-with-seam), canonical
copy assertions (no editing ephemera, RDFa kept, plain-text flavor),
native within-host copy, cut with merge + one-undo restore.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The figure/table dialog saves predated the content-model era: they
inserted after the TOP-LEVEL block (insertAfterBlock), so a table
requested from an empty list item landed after the whole list - valid
markup, wrong place - while the +paragraph/+list buttons already nested
per the model. The dialogs now record the invoking HOST (insertHost)
and both saves place through local:insert-block-at-caret: an empty list
item or cell grows the composite INSIDE itself (li/td are %Flow;), a
sibling where the parent admits it, after the top-level block otherwise.
Chrome injection rides the helper's top-level guard - the old
unconditional injection would have put a drag handle on a nested
composite. The slash table path also gets the toolbar's 3x3 + header
defaults (it only focused the input).

notion.mjs: the reported shape (slash table in an empty item nests in
the item, no chrome on the nested composite, caret in the first cell,
one undo removes it) and figure-in-cell placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser confines a native drag-selection to the contenteditable
host it starts in, so no mouse gesture could create a multi-block
selection (two-stage Ctrl+A was the only real one; select.mjs masked
the gap by building ranges programmatically). Synthesize the gestures
in select.xsl: mousedown arms a sweep anchor (caretRangeFromPoint,
caretPositionFromPoint fallback), mousemove rebuilds the selection
anchor-to-pointer via setBaseAndExtent once the pointer leaves the
anchor host (backward selections keep the anchor fixed), clamped to
one region, with viewport nudging replacing the dead native
autoscroll. Shift+Click extends from the standing anchor and re-arms
for Shift+drag; Shift+Up/Down extend block-granularly past host edges
(native within the host). In-host drags stay native, the drag handle
never arms a sweep, sweep state clears on mouseup/dragstart/undo
restore. Downstream machinery (delete machine, type-to-replace,
canonical copy/cut) consumes the selections unchanged.

New select.mjs cases drive the gestures with real mouse events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@namedgraph namedgraph merged commit bff681f into master Jul 12, 2026
2 checks passed
@namedgraph namedgraph deleted the feature/editor-quick-wins branch July 12, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant