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 sourceA 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.
| Bound | Value |
|---|---|
| Areas / editor tabs | 12 / 32, bounded split depth |
| Floating panels | 6; the last docked panel cannot float |
| Named layouts | 12, profiles capped at 512 KiB (tunetoys.workspace-layouts.v1) |
| Layout Undo history | 30 session steps |
| Closed-panel cache | 16 per Toy |
| Per-Toy workspace record | 2 MiB (tunetoys.workspace.v1) |
| Editor Back/Forward trail | 64 locations |
| Toy Undo history | 100 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.
| Section | Items |
|---|---|
| Close | Close, Close others, Close to the right, Close group. Closed panels enter the sixteen-entry recovery list; one layout Undo step per command |
| Keep | Pin / 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 |
| Arrange | Split 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 |
| Related | Editor 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 |
| Recover | Reopen 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.
| Component | Available behavior | Workspace decision |
|---|---|---|
| Splitter | Nested horizontal/vertical layouts, controlled sizes, CSS-unit bounds, collapsible panes and keyboard resizing | Renders every split in the dock tree. Splitter panes remain direct children as required by its API. |
| Tabs | Controlled active tabs, keyboard navigation, optional inactive-panel unmounting | Provides each area's tab navigation. The host owns tab identities, placement and editor state. |
| Menu | Submenus, keyboard interaction and anchored positioning | Provides area actions and shallow Arrange/Organize/Clipboard/Select-layer menus. |
| FloatingWindow | Viewport-constrained windows and pointer/keyboard resize handles | Renders floating editor/tool panels with resizing. A shared host pointer controller owns drag previews, docking, keyboard movement, identity and persistence. |
| Menubar and Toolbar | Application menu and grouped command surfaces | Menubar implemented: the application menu holds the editor's commands and the dock's layout work; Toolbar remains unused. |
| ScrollArea | Independent scroll viewports and viewport refs | Available when custom scrollbars are useful. Dock bodies use native overflow scrolling. |
| AppShell | Outer header/sidebar/footer layout | Used 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 shortcut | Action |
|---|---|
| Arrows | Focus neighboring panel |
[ / ] | Previous / next tab |
| Enter | Maximize / restore |
| T | Reopen last closed panel |
| Z / Y | Layout Undo / Redo |
| L | Open the layout chooser |
| F | Float / 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#
| Feature | Behavior |
|---|---|
| View | The accepted portable document |
| Edit JSON | Isolated 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 |
| Find | Cmd/Ctrl+F literal key/value search with counts, Enter/Shift+Enter navigation, Escape to close |
| Reveal in JSON | From node and Frame/layer menus; opens or reuses a Source panel and focuses the exact path; nested instances reveal their definition's source |
| Context bar | Breadcrumbs 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 markers | Short-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 |
| Preferences | Expanded branches, query/result and scroll per Toy/view under tunetoys.source-view.v1; admission prunes stale paths; blocked storage keeps a bounded session fallback |
| Diagnostics | Import/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#
| Check | Covers |
|---|---|
apps/web/test/dock-layout.test.mjs | Moves/grouping/pruning, ordering, malformed restoration, source separation, duplicate-view cameras/selections, bounded nesting, storage |
apps/web/test/workspace-control.test.mjs | Nested 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.mjs | Guide 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.mjs | Tab 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 fixtures | JSON drafts, find/reveal, context bar, history diffs and saved-copy reverts |
| Browser review | Pointer 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
Related#
- Patch editor
- App shell, Settings and Library
- Appearance — layer folders, multi-selection and bulk styling in the Frame designer
- Revisioned authoring operations
- Hotwire / synth.ist UX audit