# AGENTS.md — @dolphinspired/solid-stage Compact briefing for an AI agent *using* this library in a consumer app. Full human docs are in [`docs/`](./docs/README.md); this is the fast path plus the assumptions you can safely make. (If you are *developing this library itself*, read [`docs/architecture.md`](./docs/architecture.md) and `CLAUDE.md` instead.) ## What it is Editor-chrome primitives for **SolidJS** (peer: solid-js ^1.9): collapsible side panels, collapsible sections, a canvas stage, a full set of compact form controls, and three app-level overlays (tooltip / modal / toast). Browser-only (no SSR). Targets property-heavy editor UIs (a canvas in the middle, dense settings on the sides). ## Non-negotiable setup 1. `import '@dolphinspired/solid-stage/styles.css'` once. 2. Wrap the app in an element with **`class="stage-root"`**. All tokens are scoped to it and all portalled UI mounts into the nearest one. Nothing themes correctly outside a `.stage-root`. 3. Mount overlay hosts **once**, inside the `.stage-root`, for any overlay you use: ``, ``. (The tooltip self-mounts.) The dock model adds `` + ``. 4. On Vite, set `optimizeDeps: { exclude: ['@kobalte/core'] }`, or a Kobalte popup such as the `ColorField` picker throws `React is not defined` in dev. See [`docs/quickstart.md`](./docs/quickstart.md). ## Read this before composing UI [`docs/guides.md`](./docs/guides.md) explains how to put the pieces together. The mistakes that come up most in real apps: - **Stacked fields go in a `FieldGroup` or `Fieldset`.** Rows have no margin, and a section body adds no spacing, so bare rows touch with no error. Bare custom content is fine. - **A value only the app changes is a `ReadoutField`,** not a `readonly` input with a no-op `onChange`. - **A sentence in a `Label` needs `layout="expanded"`,** or it truncates to one line. - **Custom CSS reads tokens.** Don't copy their values, and don't add `var()` fallbacks, which only hide misspelled names. - **`TextField` fires `onChange` per keystroke.** Keep a draft if the handler does real work. - **Show state with `active`,** not by swapping `variant`. - **A dock section's `render` reruns on every move,** so state that must survive goes outside it. - **A panel that opens on demand is an app panel** (`appPanels` option; its sections name it as `dock`): open it with `layout.openPanel(id)` and close it with `layout.closePanel(id)`. Its sections never leave it. `closable: false` drops its ×; `framed: false` drops its header strip (the × moves to the top section, and the panel stays at its declared `x`/`y`). Don't park sections in a dock that nothing renders. - **Global shortcuts must step aside for library controls.** `SelectField` is a `button` with a portaled list, so checking for `INPUT` and `SELECT` isn't enough. ## The one distinction that matters: Control vs Field Every form widget exists twice: - **`XField`** — captioned form **row** (label + control + description/info/ validation). Put these inside a `Section`. *This is the default choice.* - **`XControl`** — the bare widget only. Use when placing a widget yourself (toolbar, custom layout). Pairs: `TextField`/`TextControl`, `NumberField`/`NumberControl`, `TextareaField`/`TextareaControl`, `ColorField`/`ColorControl`, `BooleanField`/`BooleanControl`, `SliderField`/`SliderControl`, `SelectField`/`SelectControl`, `SearchField`/`SearchControl`, `ButtonGroupField`/`ButtonGroupControl`. Buttons are actions, not values — no Field/Control split: `IconButton`, `IconToggle`, `ActionButton`, `ToggleButton` (plus the icon-set fields `IconButtonField` / `IconToggleField`). `Label` is display-only text, and `ReadoutField` is a caption plus a value only the app changes. Grouping containers (no value of their own): `FieldGroup` stacks rows; `Fieldset` adds an inline caption+rule heading over the stack (the rung between `FieldGroup` and a full `Section`); `FieldBar` lays fields out horizontally for a toolbar (`grow` makes the cluster fluid). `TextField`/`TextControl` take `actions` — a row of buttons (icon or label, `InputAction[]`) seamed to the input's right edge for copy / apply / reset (the same primitive as the `SearchField` ◀▶ stepper). ## Assumptions you can rely on **Reactivity convention — values are accessors.** A widget's `value` is a *getter function* `() => V`, not a value. Likewise `readonly`/`disabled` are `() => boolean`. `onChange` is a plain `(next: V) => void`. ```tsx const [n, setN] = createSignal(0); // value={n}, NOT value={n()} ``` Array/record props are passed **by value**, not as accessors. For `SelectField` `options`/`optionLabels`, keep them **stable** — wrap a store/signal-derived array in `createMemo`, never a bare getter that re-reads reactive state on every access (that footgun once recursed Kobalte's Select into a stack overflow; the library now memoizes defensively, but it's still the right habit). Button option arrays can read signals inline, since they render by position and keep keyboard focus. **Every `*Field` accepts:** `name` (caption; `""` = no label), `value`, `onChange`, and optionally `readonly`, `layout` (`'compact'|'expanded'`), `description`, `info` (shows an `(i)` bubble), `id` (deep-link handle), `validate`. **Every `*Control` accepts:** `value`, `onChange`, `readonly`, `aria-label`, `level` (`'error'|'warn'`), and the validation handlers `onFocusIn`/`onFocusOut`/ `onPointerEnter`. **Controlled + uncontrolled, mirrored everywhere.** Stateful things (panel/section collapse, tabs) take optional state-accessor + change-handler; omit them to let the component self-manage and persist. A controlled side panel can be asked to close by the library: on a stage too narrow for both panels, `EditorPanels` calls `onLeftToggle(true)` / `onRightToggle(true)` (`exclusiveWhenNarrow`, default on). **Theming is CSS variables.** No inline-style colour/size props. Override `--stage-*` tokens on `.stage-root`. Defaults are zero-specificity, so overrides win without `!important`. Three size tiers via `size="small|normal|large"` or a `.stage-size-*` class; they cascade and scale font + control height together. **Readonly:** pass `readonly={() => true}`. Maps to the native readonly attr or disabled; shown via colour, drops out of tab order. **State helper:** `createEditorStore({ defaults, persist? })` returns `[state, setState, bind]`. Spread `{...bind('path', 'to', 'field')}` into any field to fill `value` + `onChange`. React to changes with `createEffect`. Don't expect a subscribe API — it's Solid. **Color values** are hex **without** `#`, UPPERCASE (`'8AF'` / `'8A8A8A'`, 8 digits with `alpha`). The input boxes display a leading `#` — presentation only, never part of the stored value. **Icons:** `import { Copy, Lock, X, ... } from '@dolphinspired/solid-stage/icons'`. Every `icon` prop is an `IconComponent` (= `Component`, i.e. any component renderable with no props) — pass the component itself (`icon={Copy}`), bundled or your own; never call it. Sizing/colour is CSS (`.stage-icon` / `--stage-icon-size` / `--stage-color-icon`), not a prop. For "any icon by name", the `stageIcons()` preset (`@dolphinspired/solid-stage/vite`) wires `unplugin-icons` so `import X from '~icons//'` works tree-shaken + auto-`.stage-icon`. ## Common shapes (copy these) ```tsx // Minimal stage with a store-bound field const [state, setState, bind] = createEditorStore({ defaults, persist: { key: 'app', version: 1 } });
} leftPanel={
} />
// Imperative overlays (call from anywhere) stageToast.push({ message: 'Saved', level: 'success' }); stageModal.open(() => ); // a THUNK, not ``` ## Gotchas - `value={n}` not `value={n()}` — pass the accessor. - `stageModal.open` and `stageToast.push({ message })` take **thunks** (`() => JSX`), not eager JSX (they're called outside a Solid owner). - Overlays need their host mounted once, or nothing appears. - The dock model's `SectionDef.render` is a thunk, and `DockZone`/`useSectionLayout` must run *under* `SectionLayoutProvider` (not in an eager top-level `const`). - `BooleanField` does not wrap `BooleanControl` (label-association reasons) — both exist, just not by composition. ## Full docs [`docs/README.md`](./docs/README.md) indexes everything: quick start, concepts, the full control/layout/state/overlay/dock reference, theming, icons, guides (custom control, validation, custom search, deep-linking), and consumer testing (the Kobalte `.jsx` Vitest inline recipe).