feat(ui): add the editor shell - persistence, outline, inspector and insert #33

Merged
TJC merged 9 commits from feat/m6-editor-shell into main 2026-08-18 16:20:02 +01:00
Collaborator

Summary

  • Named templates in localStorage under one key, parsed entry by entry so a bad one is dropped and reported rather than taking the app with it; autosaved with every change (#18)
  • Outline tree down the left: reorder, indent, outdent and delete, with selection syncing both ways to the sheet and error and overflow badges on the offending node (#19)
  • Inspector generated from an inputs schema for every node - containers and marks get the schema they would have declared, so one control renderer serves them and every widget alike, and ink is a theme token select with no hex escape hatch (#20)
  • Two-tier insert menu, Widgets first and Layout visually secondary, inserting a deep copy; unpack to layout labelled one way and behaving that way (#21)
  • Every edit is parsed before it is committed, so localStorage never holds a template the app cannot reopen; an edit that would not parse is refused with the reason. Insert retries once at grow, so a notes block absorbs leftover space rather than being refused for it

Test plan

  • pnpm test - 498 passing, including the milestone exit gate: compose a four-widget page from scratch, title a todo list, reload, get it back
  • pnpm lint, npx tsc --noEmit, pnpm build clean
  • Headless coverage for the tree moves, the generated schema per node kind, the blank nodes the Layout group inserts, and the per-entry library parse
## Summary - Named templates in localStorage under one key, parsed entry by entry so a bad one is dropped and reported rather than taking the app with it; autosaved with every change (#18) - Outline tree down the left: reorder, indent, outdent and delete, with selection syncing both ways to the sheet and error and overflow badges on the offending node (#19) - Inspector generated from an inputs schema for every node - containers and marks get the schema they would have declared, so one control renderer serves them and every widget alike, and ink is a theme token select with no hex escape hatch (#20) - Two-tier insert menu, Widgets first and Layout visually secondary, inserting a deep copy; unpack to layout labelled one way and behaving that way (#21) - Every edit is parsed before it is committed, so localStorage never holds a template the app cannot reopen; an edit that would not parse is refused with the reason. Insert retries once at grow, so a notes block absorbs leftover space rather than being refused for it ## Test plan - `pnpm test` - 498 passing, including the milestone exit gate: compose a four-widget page from scratch, title a todo list, reload, get it back - `pnpm lint`, `npx tsc --noEmit`, `pnpm build` clean - Headless coverage for the tree moves, the generated schema per node kind, the blank nodes the Layout group inserts, and the per-entry library parse
- one key holds the library, parsed entry by entry so a bad one is dropped
- create, rename, switch and delete from a bar above the sheet
- a rejected entry is named in a notice rather than lost in silence

closes #18
- reorder, indent, outdent and delete from the outline, not the sheet
- a widget node is one row and nothing under it is selectable
- every edit is parsed before it is committed, and refused with the reason
- error and overflow badges land on the node that caused them

closes #19
- containers and marks get the schema they would have declared, so one
  control renderer serves them and every widget alike
- ink, stroke and font size are theme token selects; no colour control
- a slot holding an expression is shown rather than overwritten

closes #20
- Widgets first, Layout second and visually secondary, same tree underneath
- insert deep-copies the spec and lands relative to the selection
- a notes block grows into the space left over rather than being refused
- unpack is labelled one way and behaves that way

closes #21
- adding to a list read input.default, so an edited list added a stale row
- the resolver and renderer already paint one; only the panel was missing it
- htmlFor pointed at the div holding the rows, so the label named nothing
- two entries broken the same way under one name said the same thing, and
  collided on the key
- the notice interpolated undefined where the other two guard it
Author
Collaborator

Review fixes, one commit each:

  • f57f6e6 a new list row copies the last row on screen - it read the schema default, so an edited list added a stale row
  • 887d09e containers get a Fill slot - the resolver and renderer already painted one, only the panel was missing it
  • 1bb5c41 a list control is named as a group - htmlFor pointed at the div holding the rows, so the label named nothing
  • 33aa1c1 notices keyed by position - two entries broken the same way under one name said the same thing and collided
  • cae08cf a rejected entry with no issue says "no reason given" rather than interpolating undefined

Not taken: the stale selection left behind by a delete.

  • insertNode treats an unresolvable target and no target identically - find misses, !into, append to the root - so clearing the selection changes the tree not at all
  • the sheet overlay, the inspector panel and the outline buttons all read the id and degrade to nothing found, the same as no selection
  • checked by running the scenario both ways: same outline either side
Review fixes, one commit each: - `f57f6e6` a new list row copies the last row on screen - it read the schema default, so an edited list added a stale row - `887d09e` containers get a Fill slot - the resolver and renderer already painted one, only the panel was missing it - `1bb5c41` a list control is named as a group - `htmlFor` pointed at the div holding the rows, so the label named nothing - `33aa1c1` notices keyed by position - two entries broken the same way under one name said the same thing and collided - `cae08cf` a rejected entry with no issue says "no reason given" rather than interpolating undefined Not taken: the stale selection left behind by a delete. - `insertNode` treats an unresolvable target and no target identically - `find` misses, `!into`, append to the root - so clearing the selection changes the tree not at all - the sheet overlay, the inspector panel and the outline buttons all read the id and degrade to nothing found, the same as no selection - checked by running the scenario both ways: same outline either side
TJC requested review from TJC 2026-08-18 16:19:51 +01:00
TJC approved these changes 2026-08-18 16:19:59 +01:00
TJC merged commit 91ba4d293d into main 2026-08-18 16:20:02 +01:00
Sign in to join this conversation.
No reviewers
TJC
No labels
chore
core
render
ui
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TJC/daily-planner-template!33
No description provided.