# 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).