diff --git a/CLAUDE.md b/CLAUDE.md
index 19781ce..dbff073 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,9 +35,10 @@ packages/solid-stage/ # the published artefact
src/
index.ts # public exports only — keep narrow
test-utils.ts # mount() helper — internal, NOT in index.ts
- panes/ # EditorPanes, SidePane, Module, size.ts
+ layout/ # EditorPanels, Panel, Section, Toolbar, TabMenu, size.ts
canvas/ # CanvasStage, layers.ts
- fields/ # Field + FieldGroup, value fields, button family,
+ controls/ # Field + FieldGroup + FieldBar, *Control widgets,
+ # *Field wrappers, button family, Label,
# createFieldValidation, InfoIcon
overlay/ # stageTooltip, modal
state/ # createEditorStore, createPersistedToggle, persist.ts
@@ -67,10 +68,10 @@ that's the global default (explicit > implicit).
happy-dom 15.x doesn't ship a `ResizeObserver` or `IntersectionObserver`.
`CanvasStage` source guards with `typeof ResizeObserver !== 'undefined'`;
-`CanvasStage.test.tsx` stubs it via `vi.stubGlobal`. Likewise `SidePane` tracks
+`CanvasStage.test.tsx` stubs it via `vi.stubGlobal`. Likewise `Panel` tracks
overflow with an `IntersectionObserver` over top/bottom sentinels (guarded the
same way, NOT a scroll handler — an `onScroll` that positions elements is a
-scroll-linked effect Firefox warns about); `SidePane.test.tsx` stubs
+scroll-linked effect Firefox warns about); `Panel.test.tsx` stubs
`IntersectionObserver` and drives the callback by hand.
Kobalte fields work under happy-dom, but the event that drives a value change
diff --git a/NEXT-UP.md b/NEXT-UP.md
index e7e5730..7d09175 100644
--- a/NEXT-UP.md
+++ b/NEXT-UP.md
@@ -18,14 +18,14 @@ design driven by the next real consumer.
`PageSelector` exists only in `osrs-engine-study`. Greenfield in the library.
-## Module strip-icon bar
+## Section strip-icon bar
-Collapsed `SidePane` currently shows only the chevron. Later: a vertical
-column of module icons (consumer-provided list). Requires deciding on an
+Collapsed `Panel` currently shows only the chevron. Later: a vertical
+column of section icons (consumer-provided list). Requires deciding on an
icon-pack approach without bloating the dep surface.
-NB: this is **not** resolved by `Module`'s `icon` prop (a header-chevron
-override on an *expanded* module) or `SidePane`'s `toggleVariant="edge"` (an
+NB: this is **not** resolved by `Section`'s `icon` prop (a header-chevron
+override on an *expanded* section) or `Panel`'s `toggleVariant="edge"` (an
edge collapse toggle). Those are separate, shipped features; this item is still
the collapsed-strip vertical icon column.
diff --git a/README.md b/README.md
index 7d9d4d6..62a9ddd 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# @nuffle/solid-stage
-Small library of Solid + Three editor chrome primitives — collapsible side panes,
-sectioned modules, and a multi-layer canvas stage. Extracted from two consumer
-apps that independently grew the same shape.
+Small library of Solid + Three editor chrome primitives — collapsible side
+panels, collapsible sections, and a multi-layer canvas stage. Extracted from two
+consumer apps that independently grew the same shape.
## Install
@@ -17,6 +17,29 @@ Configure your registry to resolve the `@nuffle` scope:
@nuffle:registry=https://gitea.homelab.nuffle.me/api/packages/nuffle/npm/
```
+## Controls & Fields
+
+The library splits its interactive widgets into two tiers:
+
+- A **Control** is the bare widget — a value (`value` / `onChange`), its
+ widget-specific props (`min`/`max`, `options`, `alpha`, …), and nothing else.
+ No caption, no field row. Drop one straight into a toolbar or compose your own
+ layout. Seven ship: `TextControl`, `NumberControl`, `TextareaControl`,
+ `SliderControl`, `SelectControl`, `ColorControl`, `BooleanControl`.
+- A **Field** is the captioned, value-bound row that *composes* a control — it
+ adds the label, the layout (`compact` / `expanded` / `bar`), the description,
+ the info bubble, and validation, then renders the matching control inside.
+ Each `*Field` is the `*Control` plus that field chrome: `TextField`,
+ `NumberField`, `TextareaField`, `SliderField`, `SelectField`, `ColorField`,
+ `BooleanField`.
+
+The control is the single source of truth; the field never re-implements the
+widget. (`BooleanField` is the one exception — its caption must live inside the
+Kobalte root for click-to-toggle, so it keeps a bespoke label rather than
+wrapping `BooleanControl`.) `Field` / `FieldGroup` / `FieldBar` /
+`createFieldValidation` are the shared field-row machinery; `Label` is bare
+display text (neither field nor control).
+
## Type scale & row height
The library is opinionated about layout: **it owns the type scale (font-size +
@@ -36,19 +59,19 @@ the 640px breakpoint (iOS Safari zoom guard).
**The tiers cascade.** The tokens are CSS custom properties, so a tier set on any
level flows down to everything inside it, and a nested level can re-tier to
-override its ancestor — stage → pane → module → field. Set it with a `size` prop
-(`'small' | 'normal' | 'large'`) at each level instead of the class:
+override its ancestor — stage → panel → section → field. Set it with a `size`
+prop (`'small' | 'normal' | 'large'`) at each level instead of the class:
-- `EditorPanes` — `size` (whole stage), `leftSize` / `rightSize` (one pane).
-- `SidePane` — `size` (the pane: header + modules + fields).
-- `Module` — `size` (the module's **fields only**; its header keeps the pane
- size, so an info-dense module can shrink its rows without its header falling out
+- `EditorPanels` — `size` (whole stage), `leftSize` / `rightSize` (one panel).
+- `Panel` — `size` (the panel: header + sections + fields).
+- `Section` — `size` (the section's **fields only**; its header keeps the panel
+ size, so an info-dense section can shrink its rows without its header falling out
of line).
Use the explicit `.stage-size-normal` class (or `size="normal"`) to reset a
subtree back to normal under a `small` / `large` ancestor.
-The module header and the toolbars are part of the grid too: a header is one
+The section header and the toolbars are part of the grid too: a header is one
control-height, and a bar's height tracks the control-height of the controls it
hosts.
@@ -108,25 +131,25 @@ stageTooltip.hide(el);
```
Positioning is handled by `@floating-ui/dom` with `strategy: 'fixed'`, so the
-bubble escapes pane `overflow` clipping. The single host element re-parents into
+bubble escapes panel `overflow` clipping. The single host element re-parents into
the triggering field's nearest `.stage-root` so that root's token overrides
cascade.
## Modal
A single app-level modal for settings-style flows: open it, edit, close it. It
-dims the page, centres one rounded container hosting a `Module`, traps focus, and
+dims the page, centres one rounded container hosting a `Section`, traps focus, and
restores it to the opener on close. Clicks on the dim neither close the modal nor
-pass through to content beneath; closing is the module's `×`, the Escape key, or
+pass through to content beneath; closing is the section's `×`, the Escape key, or
`stageModal.close()`.
Render `StageModalHost` **once**, inside your `.stage-root`. Then open it from
anywhere with a thunk (a function returning the content — not pre-built JSX, so it
-mounts under the host's reactive owner). The hosted `Module` takes an `onClose`
+mounts under the host's reactive owner). The hosted `Section` takes an `onClose`
prop, which renders its `×` and makes the header static (no collapse):
```tsx
-import { StageModalHost, stageModal, Module, FieldGroup, TextField } from '@nuffle/solid-stage';
+import { StageModalHost, stageModal, Section, FieldGroup, TextField } from '@nuffle/solid-stage';
// once, near the app root:
@@ -134,11 +157,11 @@ import { StageModalHost, stageModal, Module, FieldGroup, TextField } from '@nuff
// from any handler:
stageModal.open(
() => (
- stageModal.close()}>
+ stageModal.close()}>
-
+
),
{ label: 'Application Settings' },
);
@@ -150,15 +173,15 @@ validation tooltips opened inside the modal portal into it and stack correctly
above the dim. It is a plain descendant of your `.stage-root`, so it inherits all
your token overrides cleanly — nothing to re-declare.
-## Module header actions
+## Section header actions
-Put icon buttons on a module's title bar (copy / lock / popout, …) with
+Put icon buttons on a section's title bar (copy / lock / popout, …) with
`headerActions` — a list of `{ icon, title, onClick, active?, disabled?, tone? }`.
Each renders as a borderless `IconButton` in the **subtle** variant (a muted glyph
whose shape lights up on hover, no accent fill when toggled on); `title` is the
accessible name and the native hover tooltip. As secondary meta-actions they are
**mouse-only** (not in the tab order, no focus ring). Clicking an action never
-toggles the module, and in a dismissable (modal) module they sit left of the `×`.
+toggles the section, and in a dismissable (modal) section they sit left of the `×`.
The `subtle` and `tabbable` props are also available on `IconButton` directly, for
secondary icon buttons elsewhere.
@@ -166,7 +189,7 @@ secondary icon buttons elsewhere.
```tsx
import { Copy, Lock } from 'lucide-solid';
-
{/* … */}
-
+
```
-## Module icon
+## Section icon
-Override a module's `▾`/`▸` collapse chevron with a custom glyph via `icon`
+Override a section's `▾`/`▸` collapse chevron with a custom glyph via `icon`
(an `IconComponent`, like `headerActions`). One icon serves both open and closed
states; swap it per-state from the consumer side via `open()`/`onToggle` if a
state-dependent glyph is wanted. The glyph tracks the size tier like an icon
@@ -188,25 +211,25 @@ button. Collapse still works exactly as before.
```tsx
import { Layers } from 'lucide-solid';
-{/* … */}
+{/* … */}
```
-## SidePane edge collapse
+## Panel edge collapse
-By default a `SidePane` (and an `EditorPanes` left/right pane) collapses via a
-toggle in a **top header strip**. Pass `toggleVariant="edge"` (on `SidePane`, or
-`leftToggleVariant` / `rightToggleVariant` on `EditorPanes`) to move the toggle to
-a **full-height vertical bar on the pane's inside edge** instead — reclaiming the
-header strip's vertical space for module content. Collapsed, the toggle fills the
-full pane height.
+By default a `Panel` (and an `EditorPanels` left/right panel) collapses via a
+toggle in a **top header strip**. Pass `toggleVariant="edge"` (on `Panel`, or
+`leftToggleVariant` / `rightToggleVariant` on `EditorPanels`) to move the toggle to
+a **full-height vertical bar on the panel's inside edge** instead — reclaiming the
+header strip's vertical space for section content. Collapsed, the toggle fills the
+full panel height.
```tsx
-} />
+} />
```
## TabMenu
-A horizontal tab strip for a bar slot (e.g. an `EditorPanes` `topSub`), modelled
+A horizontal tab strip for a bar slot (e.g. an `EditorPanels` `topSub`), modelled
on an object/document tab bar: each tab has an `×` close (middle-click also
closes), an optional `+` new-tab button rendered as a tab inline at the end of the
strip, and overflow surfaces `‹`/`›` scroll arrows. Library chrome glyphs are
diff --git a/docs/architecture.md b/docs/architecture.md
index 3cbbbc4..0694b45 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -19,11 +19,11 @@ both independently grew the same shape.
The load-bearing principles. Everything else is downstream of these; a new
feature that violates one is almost certainly wrong.
-1. **Pre-composed children, not registries.** `EditorPanes` takes
- `leftSide?: JSX.Element`, not `leftModules: ModuleId[]`; `Module` takes
+1. **Pre-composed children, not registries.** `EditorPanels` takes
+ `leftPanel?: JSX.Element`, not `leftSections: SectionId[]`; `Section` takes
`children`, not a field schema; every bar is a pre-composed `Toolbar` (or
plain JSX), not a bar config object. Naming the collapsible container
- `Module` does **not** imply a module registry / id-keyed lookup — that
+ `Section` does **not** imply a section registry / id-keyed lookup — that
mapping is app-specific. Composition stays child-based throughout.
2. **Controlled + uncontrolled, mirrored everywhere.** Anything with state
@@ -31,7 +31,7 @@ feature that violates one is almost certainly wrong.
with an `internalOpen()` signal fallback. Canonical accessor:
`isOpen = () => props.open?.() ?? internalOpen()`. In uncontrolled mode the
internal signal seeds from / writes back to localStorage where a primitive
- persists (`Module` collapse, pane collapse); controlled mode leaves
+ persists (`Section` collapse, panel collapse); controlled mode leaves
persistence to the parent (never caches).
3. **CSS classes + variables, no inline styles.** Every classname is `stage-*`.
@@ -39,7 +39,7 @@ feature that violates one is almost certainly wrong.
Consumers override by setting variables on any container. The library never
writes `!important` — `:where()` (specificity 0) lets consumer overrides win
without it. The library ships no inline-`style` escape hatch; the style hook
- is a `class` prop (e.g. `LabelField`).
+ is a `class` prop (e.g. `Label`).
4. **Icons are consumer-supplied; no bundled icon pack.** Library chrome uses
unicode glyphs (`▸`/`▾`, `«`/`»`, `×`/`+`/`‹`/`›`). Icon-button components
@@ -52,10 +52,10 @@ feature that violates one is almost certainly wrong.
SSR-safe. Documented in `README.md`. Do not add SSR shims or `typeof window`
dances beyond the one `typeof ResizeObserver !== 'undefined'` guard.
-6. **The chrome is centre-agnostic; the *world* is a canvas.** `EditorPanes`
+6. **The chrome is centre-agnostic; the *world* is a canvas.** `EditorPanels`
takes an arbitrary `center: JSX.Element` — the chrome could host any middle
(DOM page-builder preview, code buffer, node graph). Do **not** narrow
- `EditorPanes` to canvases. But `CanvasStage` and the focus model that treats
+ `EditorPanels` to canvases. But `CanvasStage` and the focus model that treats
"the world" as a single non-tabbable boundary *are* canvas-specific: the
multi-layer world/HUD/pointer z-stack + viewport ResizeObserver describe a
viewport onto a continuous spatial scene. The tell: the moment a centre is
@@ -80,7 +80,7 @@ the deferred list. Resist scope creep.
live inside a `.stage-root` subtree.
- **Portalled content mounts into the nearest `.stage-root`**, not
`document.body` — otherwise tokens don't resolve and consumer overrides don't
- cascade. It also escapes pane `overflow` clipping and sits above chrome via
+ cascade. It also escapes panel `overflow` clipping and sits above chrome via
`--stage-z-popover`. The modal extends this: lookups are
`.closest('.stage-root, .stage-modal-container')` so fields opened *inside* a
modal portal into the container (see §7).
@@ -105,7 +105,7 @@ the deferred list. Resist scope creep.
carries the whole type-colour scheme. `--stage-color-on-accent` is *not* part of
this group — it's the glyph colour on an accent fill and is pinned to contrast
the accent, not the text. No hover variant by design: the *header background*
- hover (`--stage-color-module-header-hover-bg`) is the hover affordance, not a
+ hover (`--stage-color-section-header-hover-bg`) is the hover affordance, not a
glyph colour shift — don't add a `--stage-color-text-hover` nothing consumes.
- **`--stage-colorpicker-*` is dimensions, not colour.** The ColorField popover's
width/area/track/thumb sizes use the `colorpicker` stem (no hyphen after
@@ -171,10 +171,10 @@ font-size forfeits the row-height guarantee.
`sm`/`md` width split is gone — classes kept for API compat, now equivalent).
The glyph is sized by `--stage-icon-size` (`calc(control-height − 7px)`)
applied to the `