feat(fork): Figma-style design mode for the preview browser - #50
feat(fork): Figma-style design mode for the preview browser#50NoahHendrickson wants to merge 1 commit into
Conversation
Ports the editing core of NoahHendrickson/the-forge into the in-app browser as a fork customization (manifest id fork-design-mode): - A Design toggle in the preview chrome injects a headless engine bundle (vendored Forge client, pruned of its chat/queue/MCP delivery layer) into the previewed page via webview.executeJavaScript. Selection outlines, inline-style drafts, text edit, drag-reorder, resize and delete all run in-page; the engine is guarded network-free. - The properties panel is native T3 UI docked beside the browser surface: size, padding, margin, radius, typography, fill and opacity with scrub-to-edit fields, driven over a console-message / executeJavaScript bridge. - Send delivers as a composer attachment pill (element + compact delta); the full deterministic token-aware change-request markdown is appended to the outgoing message at send time inside <design_change_request> blocks, so T3 threads are the delivery surface and the composer stays readable. Pill-only messages are sendable. - Element-to-source mapping needs forge-mode's dev-only JSX tagger in the previewed project; untagged pages get a one-click agent handoff. Vendored engine code keeps the Forge's formatting and carries t3-fork edit markers (see engine/vendor/README.md); it is its own TS island and is bundled to an injectable IIFE by the fork:design-mode-engine Vite plugin as virtual:fork-design-mode-engine. Built with Claude Code (Claude Fable 5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Thermo-nuclear code quality review
APPROVE: no — clear structural simplification available (dead vendor chrome + forked orchestrator), and PreviewView uses a heavy last-resort fence where an override fits better.
First-party integration under custom/designMode/** is mostly in good shape: protocol/stores/bridge, native ForkDesignPanel, vite IIFE plugin, and fork guards are coherent. ChatView/ChatComposer fences stay thin via forkDesignChanges — correct given fork guidance to avoid shadowing those giants.
The engine boundary is the problem. Headless mode copies DesignMode into headlessMode.ts while still vendoring the unused Forge panel stack (~5.8k lines / 18 modules, including 1288-line panel.ts and dead index.ts). That is not cheap re-sync; it is dual maintenance. Overlay still mounts Forge chrome and ships full panel CSS despite the native panel. PreviewView’s flex wrapper re-indents the whole surface tree in a watched upstream file.
See inline comments for the actionable cuts.
Sent by Cursor Automation: Thermo-nuclear PR review
| Since the native-panel split, the injected bundle is HEADLESS: `engine/headlessMode.ts` | ||
| (fork-authored, adapted from `index.ts` here) composes the vendored leaf modules and the | ||
| in-page chrome UI modules (`panel*`, `dock`, `layers*`, `canvas*`, `controls`, | ||
| `colorpicker`, `tokenpicker`, `ui/*`, and `index.ts` itself) are retained for reference | ||
| and future re-syncs but tree-shaken out of the bundle — the properties panel is native T3 | ||
| React (`custom/designMode/panel/`). |
There was a problem hiding this comment.
this preserves a lot of incidental complexity when there's a clear code-judo deletion. reachability from boot.ts/headlessMode.ts (value imports only): ~5.8k lines / 18 modules are dead (panel.ts 1288, index.ts 831, controls, colorpicker, layers*, canvas-chrome, feed-divider, …) while headlessMode.ts is a 650-line fork of the dead DesignMode in index.ts. keeping both is the opposite of cheap re-sync — every Forge edit must be ported twice, or ignored in the dead copy.
pick one boundary: (a) delete unused chrome UI from the tree and vendor only leaf modules headless actually imports, or (b) make headless a thin adapter over vendored DesignMode with panel/dock/layers stubbed so there is one orchestrator. tree-shaking the bundle does not justify shipping dead source that still has to be maintained.
| // No in-page chrome in headless mode: the T3 preview chrome owns activation and the | ||
| // native panel owns every control — the overlay contributes outlines/handles only. | ||
| overlay.toggle.hidden = true; | ||
| overlay.status.hidden = true; |
There was a problem hiding this comment.
hiding toggle/status does not make Overlay headless. Overlay still constructs Design/Copy/Before/Reset/Unlink chrome (overlay.ts ~725–730), imports DEFAULT_WIDTH from dock, and embeds the entire #panel / layers / feed CSS string into every injection.
i think there's a code-judo move here: a slim outline/handles host (or a HeadlessOverlay) so guest DOM + CSS match the native-panel split. the dual panel systems (vendor panel CSS/chrome vs ForkDesignPanel) make the boundary harder to reason about, not easier.
| {/* fork:begin fork-design-mode — see .fork/customizations.yaml#fork-design-mode | ||
| Flex wrapper so the native design panel docks as a column beside the browser | ||
| surface; the webview auto-shrinks because BrowserSurfaceSlot measures its slot. */} | ||
| <div className="flex min-h-0 flex-1 overflow-hidden"> |
There was a problem hiding this comment.
this works, but it makes the surrounding upstream path messier. the flex wrapper re-indents the entire browser surface block in a watched hot file — last-resort fencing with a large dirty hunk.
fork preference is custom > overrides > fenced upstream. PreviewView (~723 lines) is a better override target than permanently re-parenting this tree; ChatView/ChatComposer thin fences are the right call (those giants must not be shadowed). isolate the dock layout behind an override (or a thinner slot) so upstream surface edits don't conflict with a formatting-only re-indent.
| message.type === "selection" || | ||
| message.type === "drafts" | ||
| ) { | ||
| return value as DesignModeEngineMessage; |
There was a problem hiding this comment.
why does this need a cast here? checking message.type then return value as DesignModeEngineMessage accepts malformed payloads (missing fields, wrong shapes). same smell in designModeBridge.buildSend's ad-hoc cast.
make the boundary more explicit — narrow each variant's fields (or a tiny decoder) so the store/panel never see half-shaped snapshots.
| @@ -0,0 +1,1288 @@ | |||
| import type { TaggedElement } from './source' | |||
There was a problem hiding this comment.
this pushes a new file past 1k lines with zero runtime value-importers from the headless entry (only dead index.ts / type-only dock/canvas-chrome wiring). if the dead-chrome cut lands, this file should leave the tree; if you insist on a full Forge mirror for diffing, keep it out-of-tree or behind an explicit sync script, not in the shipped module graph.


Problem
Design iteration through T3 means describing visual changes in words — "make the card less cramped" — and hoping the agent guesses the right pixels. The Forge (NoahHendrickson/the-forge) solved this with click-to-edit design tooling, but as a standalone overlay with its own chat surface and delivery stack.
What this does
Brings the Forge's design mode into the in-app preview browser as a fork customization (
fork-design-mode), with T3 threads as the only delivery surface:webview.executeJavaScript. Selection outlines, inline-style draft previews, double-click text edit, drag-to-reorder, 8-point resize, and Del all run in-page. The engine is vendored Forge client code pruned of its chat/queue/MCP layer and is guard-tested network-free.console-messagelines up,window.__T3_DESIGN_MODE__commands down.div · App.tsx:15/padding-top 24px → 32px +2, × to discard, tooltip shows the full request). At send time the deterministic, Tailwind-token-aware change-request markdown is appended to the outgoing message inside<design_change_request>blocks. Pill-only messages are sendable; pills clear only when the turn start succeeds.Fork hygiene
apps/web/src/custom/designMode/; upstream touch is fenced hunks inPreviewView(.test),ChatComposer,ChatView, the two vite configs,tsconfig.json, plus oneesbuilddevDependency.engine/vendor/) keeps the Forge's formatting for cheap re-syncs, carriest3-fork:edit markers (vendor/README.md), and is its own TS island; a Vite plugin bundles it tovirtual:fork-design-mode-engine.Screenshots
UI-changes rule: before/after captures to be attached by @NoahHendrickson from the live session (panel + attachment pill).
Follow-ups (next phase, separate PRs)
Layout/auto-layout controls, stroke, typography depth; proper color picker; token picker; native layers tree; transcript chip rendering for
<design_change_request>blocks; forge-modeoverlay: falsetagging-only option upstream.Built with Claude Code (Claude Fable 5).
🤖 Generated with Claude Code