Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
629270d
docs: add approved inline text editing design spec
DavidBabinec Jun 10, 2026
f975e9a
feat(module-engine): add inlineTextEdit contract, declare on text/but…
DavidBabinec Jun 10, 2026
a0a7f24
feat(editor-store): add inline text edit session slice with coalesced…
DavidBabinec Jun 10, 2026
6bfcbf6
feat(editor-store): force-close inline edit sessions on deletion and …
DavidBabinec Jun 11, 2026
20edb1d
feat(canvas): add inline-edit typography mirroring helpers
DavidBabinec Jun 11, 2026
6bad4b7
feat(canvas): add InlineTextEditOverlay, mounted per breakpoint frame
DavidBabinec Jun 11, 2026
ee59e9f
feat(canvas): wire double-click inline editing and hide doubled text
DavidBabinec Jun 11, 2026
41b2575
docs: document canvas inline text editing (parent-doc overlay)
DavidBabinec Jun 11, 2026
a617084
docs: fix remaining stale slice counts in editor.md (11 -> 12)
DavidBabinec Jun 11, 2026
83bf384
docs: fix stale slice count in adminUi header comment (11 -> 12)
DavidBabinec Jun 11, 2026
7447335
Merge origin/main into feat/inline-text-editing
DavidBabinec Jun 11, 2026
6cfeb1a
fix(canvas): render canvas + inline-edit field in the site's fonts, n…
DavidBabinec Jun 11, 2026
61b3457
fix(canvas): stop inline-edit field scroll-shifting multi-line text
DavidBabinec Jun 11, 2026
b7b265a
fix(canvas): remove inline-edit open flash by positioning in a layout…
DavidBabinec Jun 11, 2026
a23853d
feat(canvas): rewrite inline text editing to edit the real element in…
DavidBabinec Jun 11, 2026
84b59ea
fix(canvas): stop React clobbering the inline-edit caret every keystroke
DavidBabinec Jun 11, 2026
4fe6f20
fix(canvas): let the spacebar type a space during inline editing
DavidBabinec Jun 11, 2026
3486ff0
fix(canvas): finalize inline editing — keyboard-forward guard, select…
DavidBabinec Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
docs: document canvas inline text editing (parent-doc overlay)
Replaces the "inline text editing removed" known-limitation with the
implemented design; adds the inlineEditSlice and InlineTextEditOverlay to
the editor reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  • Loading branch information
DavidBabinec and claude committed Jun 11, 2026
commit 41b257530a63b3b851587e83fc4e99cea448ed11
8 changes: 7 additions & 1 deletion docs/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ Organization is persisted in `site.explorer` on the site shell. Folders are deco

**Undo/redo** uses patch-based history: every undoable mutation captures Mutative `[next, forward, inverse]` patch pairs scoped to the `SiteDocument`. Undo applies `entry.inverse`, redo applies `entry.forward` — O(change) in both time and memory, not O(site). A 50-deep history holds kilobytes of patches instead of hundreds of megabytes of full-site clones. See [`docs/reference/editor-history.md`](../reference/editor-history.md).

The store is composed of **11 slices**, each created by a factory in `store/slices/`:
The store is composed of **12 slices**, each created by a factory in `store/slices/`:

| Slice | Owns |
|------------------------|----------------------------------------------------------------------------|
Expand All @@ -340,6 +340,7 @@ The store is composed of **11 slices**, each created by a factory in `store/slic
| `agentSlice` | AI Agent Panel state + streaming |
| `sitePanelSlice` | Dependency manifest + site runtime settings |
| `clipboardSlice` | Copy / cut / paste of layer subtrees, persisted editor-wide |
| `inlineEditSlice` | `activeInlineEdit` — the canvas inline text-edit session (double-click to edit) |

The combined `EditorStore` type lives at `store/types.ts` so each slice can import it without going through `store.ts` (this eliminates the historical store ↔ slice cycles).

Expand Down Expand Up @@ -408,6 +409,10 @@ Selection rings and hover rings are absolutely-positioned overlay divs portaled

Ring and toolbar positions are computed on each animation frame via a RAF loop (simpler than wiring ResizeObserver/MutationObserver/IntersectionObserver to every mutation source — scroll, layout shift, zoom, content animation). The loop only starts when `hasOverlayWork` is true — at least one selection ring, hover ring, selector-affinity highlight, or toolbar is visible. When there is no overlay work the effect returns early so idle breakpoint frames incur no RAF cost. **When adding a new visible overlay type to `BreakpointSelectionOverlay`, update `hasOverlayWork`** so the loop arms correctly.

### Inline text editing (double-click)

Double-clicking a node whose module declares `inlineTextEdit` (`base.text`, `base.button`, childless `base.link`) opens `InlineTextEditOverlay` — a real `<textarea>`/`<input>` in the parent document, portaled into the canvas root and RAF-tracked over the node's rect inside the breakpoint iframe (same portal + RAF pattern as the selection rings). Typography is mirrored from the live element via the iframe's `getComputedStyle`, scaled by the iframe zoom factor. Every keystroke commits live through `updateNodeProps`, so all breakpoint frames preview the edit while the session's own frame hides the node's text (`data-instatic-inline-editing` + a canvas-chrome rule). The whole burst coalesces into one undo entry; Enter/blur commit + close, Escape reverts via a single `undo()`. Session state is `activeInlineEdit` in `inlineEditSlice`. Full design: [`docs/features/canvas-iframe-per-frame.md`](features/canvas-iframe-per-frame.md) → "Inline text editing (parent-doc overlay)".

### CSS injection into the iframe

Each iframe `<head>` receives five `<style>` elements (three from `ClassStyleInjector`, one each from the others), in this order:
Expand Down Expand Up @@ -484,6 +489,7 @@ Canvas-internal values are not CSS tokens — they are raw integers intentionall
| `canvasOverlayGeometry.ts` | Cross-iframe element rect → canvas-root coords; CSS attribute value escaping |
| `canvasSelectionUtils.ts` | Selection helpers |
| `BreakpointSelectionOverlay.tsx`| Selection / hover rings, selection toolbar, inspect ladder integration |
| `InlineTextEditOverlay.tsx` | Parent-doc inline text editor floated over a node in the breakpoint iframe (double-click to edit) |
| `CanvasInsertModuleButton.tsx` | "Insert module" button in the canvas selection toolbar — opens `ModuleInserterDialog` |
| `canvasTreeLadder.ts` | Alt/Option inspect ladder tree model |
| `CanvasTreeLadderOverlay.tsx` | `useCanvasTreeLadderOverlay` — wires the ladder model to canvas events and portal |
Expand Down
24 changes: 18 additions & 6 deletions docs/features/canvas-iframe-per-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,31 @@ Live frames skip wheel/pointer/keyboard forwarding — they scroll natively, hav

---

## Plugin module sandboxing (`ModuleSandboxFrame`)
## Inline text editing (parent-doc overlay)

Plugin canvas modules render inside `ModuleSandboxFrame.tsx`, a separate component that is NOT `IframeFrameSurface`. Plugin modules run in a `sandbox="allow-scripts"` iframe with no `allow-same-origin` — they communicate with the host via `postMessage`. This is distinct from the page tree iframes described above.
Double-click a text-bearing node to edit its text in place. The editor is a real `<textarea>`/`<input>` in the **parent document** (`InlineTextEditOverlay.tsx`), portaled into the canvas root and positioned over the node inside the breakpoint iframe with the same RAF tracking the selection rings use — no cross-frame focus negotiation, which is what made the old in-iframe `contentEditable` editor unshippable.

- **Module contract:** `ModuleDefinition.inlineTextEdit?: { prop: string; multiline?: boolean }`. Declared by `base.text` (`text`, multiline), `base.button` (`label`), and `base.link` (`text`). Modules without the field keep the no-op double-click; the canvas has no per-module branches. A node with children never starts a session (`base.link` renders `text` only when childless), and dynamically-bound props are not literal-editable.
- **Session state:** `activeInlineEdit { nodeId, prop, breakpointId, multiline, initialValue, committed }` in `store/slices/inlineEditSlice.ts`. One session globally, owned by the frame that was double-clicked. Design mode only.
- **Live commit:** every keystroke calls `updateNodeProps(nodeId, { [prop]: value })`. Single-field patches coalesce under `props:<nodeId>:<prop>`, so the whole burst is ONE undo entry and every other frame previews the edit live. `startInlineEdit`/`endInlineEdit` reset `_historyCoalesceKey` so the session burst never folds into a Properties-panel burst for the same prop.
- **Hidden doubled text:** the edited node — in the session's frame only — carries `data-instatic-inline-editing`, and the canvas-chrome CSS paints it with `-webkit-text-fill-color: transparent` (NOT `color: transparent`: the overlay mirrors the element's computed `color` for the field's own text).
- **Typography mirroring:** `mirrorInlineEditTypography` (canvasOverlayGeometry.ts) copies font family/weight/style, color, text-align, and text-transform from `iframe.contentWindow.getComputedStyle(el)` and scales font-size / line-height / letter-spacing by the iframe zoom factor on every RAF tick.
- **End:** Enter or Cmd/Ctrl+Enter commits + closes — plain Enter commits even in multiline mode because `base.text` renders raw text into HTML where newlines collapse; Shift+Enter inserts a native newline for authors who add `white-space: pre-wrap` themselves. Blur commits + closes. Escape cancels: a single `undo()` iff the session committed anything.
- **Force-close:** node deleted (pruned in `pruneCanvasSelectionDraft`), document/page switch (`clearCanvasSelectionDraft`), frame unmount (breakpoint collapsed, live-mode switch), or an unmeasurable rect mid-session.

Keyboard interplay: the field lives in the parent document, so the existing `isTextInputTarget` guards already keep Delete/clipboard shortcuts out, and the field stops propagation of every keystroke so the canvas-root handler (Escape-clears-selection, duplicate, zoom keys) never sees them.

Design doc: `docs/superpowers/specs/2026-06-10-inline-text-editing-design.md`.

---

## Known limitations
## Plugin module sandboxing (`ModuleSandboxFrame`)

### Inline text editing removed
Plugin canvas modules render inside `ModuleSandboxFrame.tsx`, a separate component that is NOT `IframeFrameSurface`. Plugin modules run in a `sandbox="allow-scripts"` iframe with no `allow-same-origin` — they communicate with the host via `postMessage`. This is distinct from the page tree iframes described above.

Double-click to edit text/button content in-place was removed when the iframe move landed. The cross-frame focus model (iframe needs system focus, body competes, React StrictMode double-mount races) made every fix fragile. Text and button content is edited through the Properties panel.
---

When revisited, the shape worth considering is a parent-doc overlay positioned over the iframe element — a real `<input>`/`<textarea>` in the parent doc, no iframe focus negotiation needed.
## Known limitations

### Test environment — iframe globals

Expand Down