Customizable editor workspace

How can a creator arrange Patch, Frame Design, Preview, Source JSON, Properties, Layers and History the way they work, and trust that none of it leaks into the Toy? The workspace i

How can a creator arrange Patch, Frame Design, Preview, Source JSON, Properties, Layers and History the way they work, and trust that none of it leaks into the Toy? The workspace is a validated browser-local dock tree rendered with Mantine Splitter and Tabs, with its own layout history, pins, named layouts and floating panels. Layout state never enters a Toy, publication, compilation or authoring history.

Status: implemented browser-local docked areas, pinned tool contexts, named reusable layouts, separate layout Undo/Redo, closed-panel recovery and floating utility panels. Pop-out browser windows, piano rolls and additional editor types remain planned; see implementation status.

Structure#

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart TB
  TREE["dockLayout.ts<br/>bounded binary tree · per Toy"] --> SPLIT["Split node<br/>id · orientation · ratio"]
  TREE --> AREA["Tab-group area<br/>id · ordered panels · active tab"]
  AREA --> EDITOR["Editor panels<br/>Patch · Frame Design · Preview · Source JSON<br/>validated EditorTab, own camera/selection"]
  AREA --> TOOL["Singleton tools<br/>Properties · Layers · History"]
  TREE -.-> FLOAT["Up to 6 floating panels<br/>bounds + dock-home anchor"]
  EDITOR --> TOY[("One validated Toy<br/>one authoring history<br/>one runtime host")]
  classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
  classDef source fill:#F0442E,stroke:#1C1C1B,color:#FAFAF7
  classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
  class TREE,SPLIT,AREA shared
  class EDITOR,TOOL,FLOAT host
  class TOY source

A duplicated editor has a fresh local instance identity with separate selection, camera and Frame interaction mode; all views edit the same Toy. DockWorkspace.tsx renders the tree. Drag a tab to a strip or center guide to group it, to an edge guide to create an adjacent area, between guides or outside the dock to float it, and drag a floating header back to the same guides. A translucent preview shows the destination; Escape cancels; each accepted drag is one layout Undo step. Every tab surface shares one menu (see Tabs); it is the keyboard and discovery path for the same moves. Empty branches collapse. Presets: editor with Properties/Layers, editor beside JSON, preview above editor. Compact screens show one panel group at a time through a switcher while keeping the desktop arrangement.

BoundValue
Areas / editor tabs12 / 32, bounded split depth
Floating panels6; the last docked panel cannot float
Named layouts12, profiles capped at 512 KiB (tunetoys.workspace-layouts.v1)
Layout Undo history30 session steps
Closed-panel cache16 per Toy
Per-Toy workspace record2 MiB (tunetoys.workspace.v1)
Editor Back/Forward trail64 locations
Toy Undo history100 session edits

Tabs#

Right-click a tab, press Shift+F10 on it, click a group's … button, or right-click a floating header: the same menu appears, built by workspaceTabMenu.tsx from the current layout so every entry is enabled only when the layout model would accept it. The … button and the empty tab strip open the group form, which starts with a flat Open panel list. Middle-click closes a tab; double-click maximizes or restores its group.

SectionItems
CloseClose, Close others, Close to the right, Close group. Closed panels enter the sixteen-entry recovery list; one layout Undo step per command
KeepPin / Unpin tab (pinned editor views lead their group, hide the close control, survive group closes and persist); the main patch is permanently pinned. Duplicate view opens a second instance with its own camera and selection beside the first
ArrangeSplit right / down (a duplicate in a new adjacent area), Move to → new group on any side, an existing numbered group or a floating window; Float / Dock back; Maximize group / Restore
RelatedEditor tabs: open or focus the Patch, Design, Preview or Source view of the same instance. Properties and Layers: Follow the focused editor or pin to one editor
RecoverReopen closed tab

Workspace shortcuts use Alt+Shift so they never collide with editor keys: arrows focus neighbors, 1–9 focus a numbered group, [ / ] cycle tabs, , / . move the tab within its group, W closes, P pins, \ / − split right / down, Enter maximizes, F floats or docks, T reopens, Z / Y undo / redo layout, L opens the layout chooser. Each shortcut routes through the same command list the command search shows, so disabled states match. Menus, the command search and tooltips draw reminders with key glyphs through formatShortcut (⌥⇧W on a Mac, Alt+⇧W elsewhere); the help sheet uses the same glyphs per key.

Layout work lives in the menubar's Window menu, published by the dock as workspace-* commands: a check item per tool panel (Properties, Layers, History, Docs, Mix, Timeline, Console, Data, Files), one radio item per panel group (how compact layouts switch the visible group), Panels (focus, tabs, split, float, close), Reopen closed, and Workspace layout (choose, save, update, revert, presets and saved layouts as radio items, layout Undo/Redo, Reset). The dock has no toolbar row of its own; the save status sits beside the Toy title in the header.

Old tab-only workspaces receive a default dock tree. Admission drops invalid or stale source references, unknown panel kinds, duplicate IDs and corrupt geometry while restoring surviving views. Denied or full storage leaves the current layout usable in memory and reports the failure.

Mantine audit#

Mantine 9.7.4 was the npm registry and documentation version at this review on October 31, 2026, matching the installed packages; no upgrade was needed.

ComponentAvailable behaviorWorkspace decision
SplitterNested horizontal/vertical layouts, controlled sizes, CSS-unit bounds, collapsible panes and keyboard resizingRenders every split in the dock tree. Splitter panes remain direct children as required by its API.
TabsControlled active tabs, keyboard navigation, optional inactive-panel unmountingProvides each area's tab navigation. The host owns tab identities, placement and editor state.
MenuSubmenus, keyboard interaction and anchored positioningProvides area actions and shallow Arrange/Organize/Clipboard/Select-layer menus.
FloatingWindowViewport-constrained windows and pointer/keyboard resize handlesRenders floating editor/tool panels with resizing. A shared host pointer controller owns drag previews, docking, keyboard movement, identity and persistence.
Menubar and ToolbarApplication menu and grouped command surfacesMenubar implemented: the application menu holds the editor's commands and the dock's layout work; Toolbar remains unused.
ScrollAreaIndependent scroll viewports and viewport refsAvailable when custom scrollbars are useful. Dock bodies use native overflow scrolling.
AppShellOuter header/sidebar/footer layoutUsed for app chrome; nested editor docking remains the workspace adapter's responsibility.

Splitter handles geometry and Tabs handles activation; neither exposes editor identity, moving tabs between areas, edge docking, source-aware restoration or layout presets, so the docking model is host-owned. No external docking dependency or second Frame renderer was added.

Pins, named layouts and recovery#

Properties and Layers each have a Pin control and a Panel menu. Follow active editor uses the current editor selection; a pin binds the tool to one validated patch/Frame instance and its view state. Pins survive closing their editor and switching to JSON; selection changes update other tools pinned to the same view; source admission clears deleted targets. Canvas/property Rename uses a dialog even when Layers is pinned elsewhere; tree Rename stays inline.

Save layout stores a named arrangement: panel geometry, editor descriptors and pin targets, with no Toy document, parameter values, cameras or selections. Apply, rename and remove live in Manage saved layouts. On the same Toy valid targets return; on another Toy, Patch/JSON map to its entrypoint and Frame/Preview to its entry Frame.

Layout Undo/Redo is separate from Toy Undo/Redo and revision numbers. Splitter resize and floating drag/resize gestures each record one step; focus, tab activation and selection do not. Undo/Redo validates against the current Toy and keeps current cameras/selections. Reopen last closed panel and Recently closed restore editor identity, view state, dock position and floating bounds. A new Toy identity resets layout history.

Floating panels#

Any panel can float through Panel actions → Float panel or by dragging its tab. Floating panels keep local identity, position, dimensions and dock-home anchor per Toy; dock-back recreates the original split where it can, then falls back to a reachable group. Viewport admission keeps controls reachable when screen size changes; on compact screens floating panels join the group switcher while desktop bounds stay saved.

FloatingWorkspacePanel.tsx wraps Mantine FloatingWindow. useWorkspaceDrag.ts owns pointer capture, threshold, previews, docking and cancellation for tabs and headers; Mantine's native window drag is disabled so one controller handles the gesture. workspaceDocking.ts computes guide hit areas, tab insertion and directional focus. The move grip takes arrow keys (10 px, Shift 40 px); the resize grip supports pointer and Mantine keyboard resizing. Windows raise on interaction without touching Toy history. Recovery writes dimensions as well as coordinates because Mantine's resize handle writes inline dimensions.

Layout chooser and workspace commands#

Window › Workspace layout marks the selected preset or saved layout, and leaves it unmarked while the arrangement is modified. The visual chooser renders actual split ratios, roles, tab groups and floating bounds for the three presets and named layouts; workspacePresets.ts supplies preview and application from one constructor. Update replaces the current named arrangement; Revert reapplies its stored arrangement or the selected preset. Selection and a bounded geometry fingerprint persist per Toy; focus, titles, selections, cameras, closed-panel caches and z-order never mark a layout Modified. The fingerprint is a UI change detector, never source identity or publication authority.

Alt+Shift shortcutAction
ArrowsFocus neighboring panel
[ / ]Previous / next tab
EnterMaximize / restore
TReopen last closed panel
Z / YLayout Undo / Redo
LOpen the layout chooser
FFloat / dock

The same actions appear in command search. Input fields, editing controls, composition, menus and dialogs keep native key behavior; Toy Undo remains Cmd/Ctrl+Z. Focus changes move DOM focus as well as the workspace focus marker, and the bindings appear in shortcut help.

Editors inside the workspace#

WorkspaceEditor.tsx scopes each editor to its own tab, definition and instance path. Only the focused patch owns global graph shortcuts and clipboard listeners; inactive graph views disable React Flow deletion shortcuts and cancel previews; Frame gestures cancel when their view goes inactive. The component palette follows the retained editor when a tool panel takes focus, and palette drops carry the destination view identity so dragging into another split cannot author in the previously focused editor (see component palette boundaries).

Source JSON#

FeatureBehavior
ViewThe accepted portable document
Edit JSONIsolated text draft with live shared validation, clickable diagnostic locations and before/after review; Apply is one revision-bound source.replace edit; identity and revision cannot be edited; a changed base blocks application and keeps the draft for download/discard; closing a changed draft offers Keep editing/Discard
FindCmd/Ctrl+F literal key/value search with counts, Enter/Shift+Enter navigation, Escape to close
Reveal in JSONFrom node and Frame/layer menus; opens or reuses a Source panel and focuses the exact path; nested instances reveal their definition's source
Context barBreadcrumbs for the selected row; copies exact JSONPath (quoted bracket notation for unusual keys), a value or the enclosing node/widget/folder; Shift+F10 opens the row menu; Show in Patch/Frame centers the entity, with instance choice when needed
Change markersShort-lived added/changed/removed markers after admitted edits and history moves, capped at 256 entries, expiring after six seconds, computed in the browser UI only
PreferencesExpanded branches, query/result and scroll per Toy/view under tunetoys.source-view.v1; admission prunes stale paths; blocked storage keeps a bounded session fallback
DiagnosticsImport/snapshot and full-document validation errors keep structured paths and bounded rejected source; Review diagnostics highlights the rejected location; oversized or unsafe JSON gets diagnostics without a tree

Drafts, validation and diagnostics never affect the running audio host, and rejected documents never replace the accepted Toy.

Layers, selection and navigation#

Properties and Layers scroll independently; changing selection cannot move Layers. Canvas selection reveals folder ancestors, clears a concealing filter and scrolls the row into view. Tree and canvas hover share the active Frame view identity. Inherited lock/hide explanations name the responsible folder; preview outlines do not unhide or unlock source members. Frame context menus keep rename, duplicate, copy/cut, visibility, locks and delete at the top level, with arrangement, folder, paste and overlap selection one submenu down, all through existing actions and atomic history.

Editor Back/Forward keeps a bounded session trail of source selection/search/expansion/scroll, patch selection/camera and Frame selection/scroll. Header buttons and Alt+Left/Right restore locations, skip stale instance paths and leave portable source and both Undo histories untouched.

History#

History is a docked tab that can move anywhere while the canvas stays interactive. Selecting a retained state inspects its before/after property diff without applying it; added/removed/changed stay distinct, revision counters are excluded, and large diffs show the first 500 changes with links to full snapshot previews. Changes link to current JSON/Patch/Frame identities with explicit instance choices.

The comparison picker also compares the current Toy with its last device save or last acknowledged account draft, scoped to the Toy and account; missing references are reported, not substituted. Each saved-comparison property offers Revert to device save/account draft: it restores that value (or removes an added property) on a detached document, preserves unrelated edits and commits one undoable replacement; arrays restore together; identity/revision fields are protected; full validation rejects restores that would break references and shows the reason.

Preview before/this state opens read-only JSON or the shared Frame renderer with compiler-resolved defaults on an independent idle host that starts no audio. Restore this state is a separate explicit action using the same retained-state path and monotonic revisions as Undo/Redo. Durable history, inverse-operation serialization and collaborative reconciliation remain planned.

Rendering and persistence#

The workspace renders from external stores rather than App state. toyDocumentStore.ts holds the open Toy, its single authoring history and any gesture preview; panels subscribe to the committed document or the previewed shown document with useSyncExternalStore, so an inspector drag re-renders only the panels that draw it. workspaceLayoutStore.ts holds the layout (tabs, per-tab views and selection, dock tree) and its layout Undo/Redo; its snapshot is normalized against the committed Toy and cached per (Toy, layout) pair, and it writes the layout for the open Toy after each change (once per task). EditorWorkspace subscribes to it, as do the few chrome pieces that draw the active editor context (WorkspaceChrome.tsx: context navigation, Back/Forward, Frame clipboard and the Sound, icon and node pickers; SharedSession.tsx: the shared-session anchor and presence), each through a selector, so selection and layout changes do not re-render the App shell. audioSession.tsx holds the one runtime host and transport state.

Edits go through WorkspaceCommands (workspaceCommands.ts), created once per App: every command reads the stores when it runs rather than closing over a render, so EditorWorkspace receives one stable object and modals, errors, playback and hub navigation do not re-render editors. EditorNavigation (useEditorNavigation.ts) keeps the Back/Forward trail and reveal requests the same way and records tab switches from the layout store after each change settles; SourceNavigationProvider subscribes for the editors that consume them. The App and these modules compile under React Compiler; control flow it does not support (try/finally, throw inside try) lives in plain helpers and classes outside components.

After the first Workspace visit the App keeps EditorWorkspace mounted while Explore, Library or Settings is shown. Its root is hidden, inert and display: none, so hidden editors are not focusable, announced or hit-tested, and returning keeps the dock, React Flow canvases (store, nodes, viewport, selection) and Frame designers without rebuilding them. Effects stay alive, so global input is gated instead: the App clears the active editor while the Workspace is hidden, which turns off Patch keyboard and clipboard listeners, Source search keys and Frame gestures; DockWorkspace receives shortcuts={false}; and App-level document shortcuts (node search, Undo/Redo, editor Back/Forward) apply only while the Workspace is visible. Playback and its Shift+Space shortcut continue across pages. React <Activity> was evaluated and rejected: hiding tears effects down and React Flow resets its whole store in effect cleanup, so every node remounted on return.

Hidden editors also stop their UI work. The App wraps the mounted EditorWorkspace in WorkspaceVisibilityProvider (workspaceVisibility.tsx), which bridges to the shared renderer's FrameLiveProvider. Inside a paused region every Frame widget, node control, graphical editor, Context control and MIDI monitor subscribes to the audio host through useLiveSubscribe, which listens to nothing while paused, so the 80 ms telemetry poll no longer re-renders meters, scopes, knobs and live node values behind a hub page; SignalInspector releases its capture lease and a playing Screen video pauses. The same gate applies while the browser tab is hidden. Audio, the engine's telemetry production and the session Transport (rendered outside the workspace, with its own meter) are untouched. On return React resubscribes and re-reads each snapshot, so the first visible frame shows current meters with dock, selection, scroll and zoom intact. Frame viewports, the preview device size and the spectrogram ignore the zero size a display: none region reports, so returning paints the last layout at once instead of re-measuring from zero; other resize observers re-measure on return.

The page comes from the URL through hubRoutes.ts (useHubPage), backed by the router.ts location store, so back/forward between Workspace and a hub page shows or hides the same mounted editor rather than remounting it; the #toy= recovery fragment, ?starter=/?new=1 chooser handoff and ?collaborate= session survive page switches. Panels and dialogs that open on demand load lazily: the History and Docs dock panels suspend to a small status line inside their dock area (the area keeps its size), and App-level overlays (node Spotlight, Media library, shortcuts, About) suspend to nothing; the node Spotlight chunk is fetched shortly after the editor first mounts so keyboard opening does not drop input. The shared-session bar suspends to an identically styled placeholder so the editor does not shift.

Verification#

CheckCovers
apps/web/test/dock-layout.test.mjsMoves/grouping/pruning, ordering, malformed restoration, source separation, duplicate-view cameras/selections, bounded nesting, storage
apps/web/test/workspace-control.test.mjsNested pinned contexts across close/reload, stale-source admission, bounded layout history and gesture recovery, retained cameras/selections, closed/floating restoration and dock-home anchors, cross-Toy adaptation, malformed/blocked storage
apps/web/test/workspace-ux.test.mjsGuide geometry, forward tab insertion, floating bounds, spatial focus, shortcut admission, layout change detection, Update/Revert/adaptation, shared presets, editor floating roles
apps/web/test/workspace-tabs.test.mjsTab pins (ordering, persistence, permanent main patch), group closes skipping pins and recording recovery, keyboard tab reordering, related views, tab shortcut chords
source-draft, source-navigation, source-workflows, history-review fixturesJSON drafts, find/reveal, context bar, history diffs and saved-copy reverts
Browser reviewPointer docking/reordering, tab menus (right-click, Shift+F10, … button, floating headers), middle-click close, floating, keyboard focus/maximize, text-field guards, command search, visual presets, named Update/Revert/reload, split editors, independent cameras, compact switching, nonmodal History, pinned selection while JSON is active, phone layouts

Root npm run check remains required.

Implementation#

  • apps/web — EditorWorkspace.tsx, toyDocumentStore.ts, workspaceLayoutStore.ts, workspaceCommands.ts, useEditorNavigation.ts, audioSession.tsx, hubRoutes.ts, router.ts, DockWorkspace.tsx, dockLayout.ts, WorkspaceEditor.tsx, FloatingWorkspacePanel.tsx, useWorkspaceDrag.ts, workspaceDocking.ts, workspacePresets.ts, HistoryPanel.tsx, SourceContextBar.tsx
  • packages/authoring — history, source.replace, saved-copy comparison

Edit this page on GitHub

esc

↑ ↓ move↩ open⌘↩ new tabesc close