@xenosystem/workbench
v1.2.3
Published
Shared workbench runtime for the XENO panel platform — mounts @xenosystem/block-sdk panels from a registry into a Dockview layout, with a framework-free layout model, reconciliation, and persistence.
Readme
@xenosystem/workbench
The shared workbench runtime for the XENO panel platform — the generalization of Motion's Dockview layout system into a host-agnostic library.
It mounts @xenosystem/block-sdk panels from a PanelRegistry into a docking layout, reconciles the
registry into panes, and persists layout through a pluggable seam — so Pixel, Motion, Sound,
Canvas, and xeno-apps all get the same docking/layout runtime instead of each re-implementing
it. This is step 2 of the panel-platform build order (spec §9).
Two halves, one contract
| Entry point | What | Depends on |
|---|---|---|
| @xenosystem/workbench | The framework-free layout model, reconciler, controller, persistence, and an in-memory dock substrate. A headless consumer (agent, builder, SSR, tests) can read and drive a layout with no DOM. | @xenosystem/block-sdk only |
| @xenosystem/workbench/react | The React + Dockview rendering half: <Workbench>, the pane adapter, and hooks. | + react, dockview-react |
The spec's rule holds: layout is described in the locked PanelLayout vocabulary
(position / width / height / visible / order per panel — §9), not a new one.
Install
npm install @xenosystem/workbench @xenosystem/block-sdk
# React host also needs:
npm install react react-domQuickstart (React host)
import { createStore } from 'zustand/vanilla'
import { PanelRegistry, createAppHost, type PanelModule } from '@xenosystem/block-sdk'
import { Workbench } from '@xenosystem/workbench/react'
import { createWebLayoutStorage, WORKBENCH_THEME_TOKENS } from '@xenosystem/workbench'
import 'dockview-react/dist/styles/dockview.css' // Dockview's base layout CSS (the XENO theme is automatic)
const registry = new PanelRegistry()
registry.register(colorPanel)
registry.register(layersPanel)
const store = createStore(/* the app's existing zustand store */)
export function App() {
return (
<Workbench
registry={registry}
// pass WORKBENCH_THEME_TOKENS so panel bodies use the same chrome colors:
hostFor={(module: PanelModule) =>
createAppHost({ manifest: module.manifest, store, theme: WORKBENCH_THEME_TOKENS })}
storage={createWebLayoutStorage('myapp.workbench')}
onReady={(controller) => { /* drive menus: controller.togglePanel('layers') */ }}
/>
)
}<Workbench> applies the canonical XENO panel look automatically (see below), builds a
WorkbenchController once the dock is ready, restores any persisted layout (else seeds from each
panel's defaultSlot), and auto-saves on user drags/resizes. Each pane resolves its panel from the
registry, mounts it through the host you supply, and gets the iconic tab (lucide icon + title) for
free.
The canonical theme (zero-config)
The dark monochrome XENO chrome — rounded gapped panels, compact uppercase tab strip, lucide
icon + title tabs — is injected and applied by default; no CSS import, no theme prop. Panels
draw from the same tokens via WORKBENCH_THEME_TOKENS (both --panel-* and --xeno-* names). Opt
out with <Workbench theme="none">; a static @xenosystem/workbench/theme.css and
ensureWorkbenchThemeInjected() are exported for manual control. Ported faithfully from
xeno-motion, aligned to DESIGN_SYSTEM.md (monochromatic white accent, neutral-gray text). See
INTEGRATION.md and examples/theme-preview.html.
Tab-icon bundle-size trade-off (honest note). The zero-config default resolves manifest icons
from lucide's static icons map via a lazy import('lucide-react') (the main entry — it has a
package exports map and bundles correctly in Vite/Rollup production). It deliberately does NOT
use lucide-react/dynamic, whose extension-less star re-export Rollup collapses to an empty chunk
(lucide#2743) → blank packaged window while dev works. The cost: the static map is not
tree-shakeable, so a lazy chunk carries the full lucide set (loaded on demand, off the critical
path). We chose correctness over size for the default. To ship only the icons you use, pass a
tree-shaken renderer: renderPanelIcon={(name) => /* your mapped <Icon/> */}.
Quickstart (headless)
import { PanelRegistry } from '@xenosystem/block-sdk'
import { WorkbenchController, MemoryDockAdapter } from '@xenosystem/workbench'
const controller = new WorkbenchController({
registry, // a PanelRegistry
adapter: new MemoryDockAdapter(), // no DOM
})
controller.mount() // seeds panels by defaultSlot
controller.togglePanel('layers')
const snapshot = controller.snapshot() // serializable PanelLayout mapWhat's in the package
| Module | Exports | Purpose |
|---|---|---|
| model | PanelLayout, WorkbenchLayout, WorkbenchSnapshot, PANEL_SLOTS | The per-panel layout vocabulary + a mutable model (seed/show/hide/move/size/snapshot). |
| adapter | DockAdapter, MemoryDockAdapter, DockPanelSpec | The substrate seam + a shippable in-memory implementation. |
| reconcile | reconcile, ReconcilePlan | Pure registry × layout × current-panes → add/remove/update plan. |
| controller | WorkbenchController, WorkbenchState | The generalization of Motion's layoutStore (show/hide/toggle/switch/save/restore/reset), observable. |
| presets | WorkspacePreset, presetFromLayout, layoutFromPreset, serialize/deserializeWorkspaces | Named workspaces (Motion's WORKSPACE_LAYOUTS generalized). |
| storage | LayoutStorage, InMemoryLayoutStorage, createWebLayoutStorage | Pluggable, version-gated persistence. |
| mountPanel | mountPanel, MountedPanel | The single bridge from a PanelModule to a DOM element. |
| @xenosystem/workbench/react | Workbench, PanelPane, DockviewDockAdapter, useWorkbench, useWorkbenchState | React + Dockview rendering. |
| @xenosystem/components/chrome | PANEL_METRICS, class constants, rowClass/badgeClass/…, PANEL_PRIMITIVES_CSS, ensurePanelPrimitivesInjected | Canonical panel chrome. @xenosystem/workbench/primitives is a deprecated pointer. |
| @xenosystem/components/chrome (React) | PanelFrame, Toolbar, Section, RowList/Row, SearchField, SegmentedControl, Toggle, Field, EmptyState/LoadingState/ErrorState, Badge, StatTile, ProportionBar, Sparkline, … | The same chrome as React components. Pointer: /primitives/react. |
Panel-chrome primitives
@xenosystem/components/chrome is the canonical answer to "how does the inside of a panel look"
(@xenosystem/workbench/primitives is a deprecated pointer at that family since 1.2.0).
Before it, the chrome contract had been independently reinvented three times across the
ecosystem, and each of the five shipped canonical panels re-declared its own 100-line
const S: Record<string, CSSProperties> bag.
What was lifted, from where:
| Source | What was taken |
|---|---|
| xeno-workflow index.css | The frame contract, class names byte-identical: xeno-panel / -header / -title / -content / -content--no-header, driven by --xeno-panel-gap / --xeno-panel-radius. Workflow can delete its local copy and import ours with zero markup churn. |
| xeno-post ui/kit.tsx | The primitive vocabulary: Section, RowList/Row, Toolbar, StatTile, ProportionBar, EmptyState/LoadingState/ErrorState, StatusBadge, IconFrame — re-cut from dashboard density to in-panel density. |
| xeno-comms XenoPanel | Confirmation of the frame shape (title + actions + content). No new ideas; it validated the other two. |
| The five shipped panels | Every numeric value, by census. Where they agreed, the consensus value is used verbatim; where they disagreed, PANEL_METRICS annotates the alternatives. |
| xeno-canvas | The actionable-hint empty state — a second line that says what the thing is and how to make one, not a restatement of the emptiness. |
Three gaps the primitives close. Across all five shipped panels the census found: no hover
state anywhere, no focus-visible state anywhere (every input sets outline: none with no
replacement), and no shared disabled treatment (eleven hand-written opacity pairs). Those now
exist once.
Two structural guarantees, both test-enforced:
- Every
var()carries a literal fallback. A panel dropped into a product's own chrome with no workbench theme still renders correctly. The chain is--panel-*→--xeno-*→ literal, which is what 4 of the 5 shipped panels already use. - No Dockview, no icon barrel. A panel must not pay for a docking library. Icons are
ReactNodeprops; the three internal glyphs (SearchGlyph,ClearGlyph,ChevronGlyph) are<XenoElement>over per-id@xenosystem/elementsdeclarations (components 0.37.2) — thelucide-react/dynamicpackaged-build failure cannot recur. Chrome does import@xenosystem/elements-reactfor those glyphs; it does not import lucide.
PANEL_METRICS is the single source of truth for both the stylesheet and any JS that needs a
measurement — the CSS is generated from it. That kills a whole bug class: a virtualizer computing
scroll geometry from a JS row height while CSS sizes the row is a silent desync waiting to happen.
Deliberate divergences from DESIGN_SYSTEM.md
Flagged rather than hidden — both are dense-in-panel vs. app-chrome distinctions:
| Doc says | Panels do | Primitives ship |
|---|---|---|
| §3.1 compact controls are 24px | inspector/history/transport 18, layers 20 | controlHeightSm: 20 (default in toolbars) and controlHeight: 24 (§3.1-compliant, for panel bodies) |
| §7 badges are 18px / 10px | inspector + history 12px / 8px (byte-identical) | badgeHeight: 12 (default) and badgeHeightMd: 18 (size="md") |
Adopting the primitives therefore never makes an existing panel denser than it already is.
Migration candidates (a LATER wave — nothing is refactored yet)
Ranked by how much hand-rolled chrome disappears:
- Inspector — the largest win and the lowest risk. Its
styles.ts(280 LOC) is the only extracted style bag in the catalog and it is where the canonicalSection(20px header,rgba(255,255,255,0.03)wash, enable toggle) and theFieldlabel-column layout came from. It would deletesectionHeader/sectionTitle/sectionBody/fieldRow/search/chip/badge/iconBtn/miniBtn/textBtn/empty/scroll. - History —
Row+Badge+SearchField+StatusBar+EmptyStatemap 1:1; its badge and search recipes are already byte-identical to the primitives'. Itsxeno-history-spinnerclass, which no package currently defines, is covered by.xeno-spin/LoadingState. - Layers — same shape as History, plus it is the one panel that would keep a local override
(
--xeno-row-h: 24px) rather than adopt the 22px default. Its drop-hint borders and thumbnail stay panel-local; everything around them is primitive. - Transport — narrower:
IconButton(its 20×18 filled button is themdsize),SegmentedControlfor the readout cycle,Badgefor the chips. Its scrubber is irreducibly panel-local. - Color — the odd one out on every axis (its own
#e6e6e6/#141414tokens, a different input family,letterSpacing: 0.4unitless where everyone else usesem). Migrating it is as much a normalization as a refactor; do it last and expect visual deltas.
Two cross-cutting fixes belong to that wave, not this one: the two status reds (#ef4444 vs
rgba(239,68,68,0.65) — PANEL_COLORS now spells both, error for indicators and errorText for
text), and history's virtualizer fallback of 24 against its own default row height of 22 — a
latent desync of exactly the kind PANEL_METRICS exists to prevent.
How it generalizes Motion
| Motion layoutStore / WorkspaceManager | @xenosystem/workbench |
|---|---|
| ALL_PANELS (hard-coded PanelDef[]) | any PanelRegistry of SDK panels |
| PanelDef.defaultGroup | PanelManifest.defaultSlot |
| buildDefaultLayout (Dockview placement) | DockviewDockAdapter slot→split/tab heuristic |
| showPanel / hidePanel / togglePanel | WorkbenchController methods |
| WORKSPACE_LAYOUTS presets | WorkspacePreset (host-supplied; no domain presets baked in) |
| saveLayout / restoreLayout (localStorage, version-gated) | LayoutStorage seam (createWebLayoutStorage is version-gated) |
| panelTitles overrides | controller.renamePanel |
| zustand create store | framework-free controller + useWorkbenchState (useSyncExternalStore) |
Motion is not modified — it adopts this runtime later, at the product-adoption phase.
Scripts
npm run typecheck # tsc --noEmit
npm test # vitest run (154 tests)
npm run build # tsup → dist (esm + cjs + d.ts for '.', './react',
# './primitives', './primitives/react')
# + dist/theme.css and dist/primitives.cssSee INTEGRATION.md and examples/ (a node-runnable headless composition).
© XENO Corporation — private. Panel-platform step 2 (spec §9).
