panelize 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,618 @@
1
- # Temporary Holding Version
1
+ # Panelize
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/jayzizzle/panelize/actions/workflows/ci.yml/badge.svg)](https://github.com/jayzizzle/panelize/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ A React layout kit. You compose screens from named boxes and type — not from a className soup.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install panelize react react-dom styled-components
12
+ ```
13
+
14
+ Peer dependencies: `react` and `react-dom` 19, `styled-components` 6. Panelize ships ESM and CJS builds with TypeScript types. It is client-side UI (the build carries `'use client'`); the host owns styled-components server rendering.
15
+
16
+ ## Quick start
17
+
18
+ 1. Give the shell a height in host CSS. The library never touches `html` / `body`.
19
+
20
+ ```css
21
+ html,
22
+ body,
23
+ #root {
24
+ height: 100%;
25
+ margin: 0;
26
+ }
27
+ ```
28
+
29
+ 2. Load the fonts you want (see [Fonts](#fonts)), wrap the app once, and compose a view.
30
+
31
+ ```tsx
32
+ import { Button, Heading, Panel, PanelizeProvider, Panels, Text } from 'panelize'
33
+
34
+ export function App() {
35
+ return (
36
+ <PanelizeProvider mode='dark'>
37
+ <Panels fill>
38
+ <Panel variant='muted' padding='base'>
39
+ <Heading level={1}>Hello</Heading>
40
+ <Text muted>Your first Panelize view.</Text>
41
+ <Button label='Continue' />
42
+ </Panel>
43
+ </Panels>
44
+ </PanelizeProvider>
45
+ )
46
+ }
47
+ ```
48
+
49
+ With no `theme` prop the provider uses `defaultTheme`. To set colors, brand, and type, see [Theme](#theme).
50
+
51
+ ## Next.js (App Router)
52
+
53
+ Panelize is client UI with `'use client'` in the bundle, so you can import it from Server Components. styled-components needs its official registry, or server-rendered pages arrive **unstyled** and flash on hydrate.
54
+
55
+ ```tsx
56
+ // app/registry.tsx
57
+ 'use client'
58
+ import { useServerInsertedHTML } from 'next/navigation'
59
+ import { type ReactNode, useState } from 'react'
60
+ import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
61
+
62
+ export default function StyledComponentsRegistry({ children }: { children: ReactNode }) {
63
+ const [sheet] = useState(() => new ServerStyleSheet())
64
+ useServerInsertedHTML(() => {
65
+ const styles = sheet.getStyleElement()
66
+ sheet.instance.clearTag()
67
+ return <>{styles}</>
68
+ })
69
+ if (typeof window !== 'undefined') return <>{children}</>
70
+ return <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>
71
+ }
72
+ ```
73
+
74
+ Wrap `children` in `app/layout.tsx` with it, then use `PanelizeProvider` inside. If TypeScript complains about `stylis`, add `@types/stylis`. Host CSS for `html` / `body` height still applies.
75
+
76
+ ## Fonts
77
+
78
+ Type tokens use `family`: `base` (Open Sans), `display` (Montserrat), `mono` (system stack). **The library does not load font files.** Load them in the host, or override the stacks with `createPanelizeTheme({ fontFamilies })`.
79
+
80
+ ```html
81
+ <link
82
+ href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;400;700;800&family=Open+Sans:wght@400;700;800&display=swap"
83
+ rel="stylesheet"
84
+ />
85
+ ```
86
+
87
+ Open Sans has no weight 100; Montserrat does. Without the files, the browser falls back to `sans-serif`.
88
+
89
+ ## Goals
90
+
91
+ The layout primitives — `Panels`, `Panel`, `Rows` / `Row`, `Cols` / `Col`, `Grid` / `Cell`, `Div` — are the building blocks for app and site views, and for the custom components those views are made of. `Text`, `Heading`, `Span`, and `TextLink` sit with them: copy is part of the map.
92
+
93
+ The JSX should read as the picture. A column of rows, a sidebar beside a scrolling pane, a title then a muted line then two buttons — you should know **where** things sit without opening the browser. That only holds if the tree stays small and the vocabulary stays small (`fill` vs `flex`, `stack` as “at most this wide”). It is **layout-semantic**, not HTML landmarks (`header` / `nav` still belong to the host).
94
+
95
+ Appearance is not a pile of color props on every box. **`createPanelizeTheme`** is how you author schemes (`dark` / `light`), panel surfaces (`muted` / `base` / `highlighted`), brand, and type. **`PanelizeProvider`** puts that object on the tree. **`usePanelizeTheme`** is how views and custom components stay on the same colors — the active scheme’s surfaces, plus shared brand and status — so building UI stays standardized. Nested `Panel` variants publish `--panelize-*` so children inherit the surface they sit on.
96
+
97
+ Forms, buttons, and chips ship with the package so a host can use a complete kit. They are bonus. The core is panels, tracks, grid, and text.
98
+
99
+ This README is the human usage guide. Hover a component in the IDE for props.
100
+
101
+ You pass a theme object in. The library does not read a config file from disk, does not take over the host’s styled-components `ThemeProvider`, and does not set `html` / `body`.
102
+
103
+ ```tsx
104
+ <Panels fill scroll='vertical'>
105
+ <Panel variant='muted' minHeight='100%'>
106
+ <Rows minHeight='100%' gap='base' alignX='center' alignY='center'>
107
+ <Row>
108
+ <Heading level={1} uppercase color='primary'>
109
+ Panelize
110
+ </Heading>
111
+ </Row>
112
+ <Row>
113
+ <Text muted>A React panel layout library.</Text>
114
+ </Row>
115
+ <Row>
116
+ <Cols gap='small'>
117
+ <Col>
118
+ <Button label='Deploy now' />
119
+ </Col>
120
+ <Col>
121
+ <Button outline label='Details' />
122
+ </Col>
123
+ </Cols>
124
+ </Row>
125
+ </Rows>
126
+ </Panel>
127
+ </Panels>
128
+ ```
129
+
130
+ That tree is the picture: a full-bleed muted panel, a centered column, a title, a muted line, two actions in a row.
131
+
132
+ Every public component accepts React `style` and a `css` string (appended last, including `:hover`). `NumberInput` inherits those from `Input`.
133
+
134
+ Full theme keys: `examples/panelize.theme.md`.
135
+
136
+ ## What it is not
137
+
138
+ - **Not HTML landmarks.** `Panels` / `Panel` are layout, not `header` / `nav` / `main`. Add those in the host.
139
+ - **Not a runtime linter.** It does not warn when you break the composition rules (`Row` outside `Rows`, and so on). Follow them anyway; that is how the tree stays readable.
140
+ - **Not a global style layer.** No CSS reset, no `html` / `body` rules, no takeover of the host’s styled-components `ThemeProvider`.
141
+ - **Not a font loader or a scheme toggle.** The host loads fonts and owns `mode` state.
142
+
143
+ ## Stability
144
+
145
+ Panelize is `0.x`. The public API (`src/index.ts`) can still change in a minor release. Pin an exact version in production and read the [changelog](CHANGELOG.md) before upgrading.
146
+
147
+ ## Browser support
148
+
149
+ Evergreen browsers: **Chrome / Edge 119+, Safari 16.5+, Firefox 121+**. The floor comes from CSS the components use: container queries (`stack`), `color-mix()` (chips, switches, fields), `:user-invalid` (field validation), and `:has()` (`Label`).
150
+
151
+ `Heading` and `Label` also use `text-box: trim-both cap alphabetic` to hug glyphs. Chrome 133+ and Safari 18.2+ support it. Firefox does not yet, so there the box keeps its normal line-height padding. Layout still works; spacing is slightly looser. Older browsers lose the features above rather than the layout.
152
+
153
+ ## Documentation
154
+
155
+ | For | Read |
156
+ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | Composition rules | [`docs/composition.md`](docs/composition.md) |
158
+ | Copyable layouts | [`docs/recipes.md`](docs/recipes.md) |
159
+ | Every export at a glance | [`docs/components.md`](docs/components.md) |
160
+ | Shell, layout, text, theme | [`docs/shell.md`](docs/shell.md), [`docs/layout.md`](docs/layout.md), [`docs/text.md`](docs/text.md), [`docs/theme.md`](docs/theme.md) |
161
+ | Forms and value ownership | [`docs/forms.md`](docs/forms.md) |
162
+ | Accessibility | [`docs/accessibility.md`](docs/accessibility.md) |
163
+ | Every theme key | [`examples/panelize.theme.md`](examples/panelize.theme.md) |
164
+ | Props | Hover in the IDE, or `dist/index.d.ts` |
165
+
166
+ ## For agents
167
+
168
+ **Using Panelize in an app:** read [`llms.txt`](llms.txt) then [`docs/composition.md`](docs/composition.md). After `npm install panelize`, those files are `node_modules/panelize/llms.txt` (export `panelize/llms.txt`) and `node_modules/panelize/docs/`. One-file dump: `llms-full.txt`.
169
+
170
+ Copy a ready-made rule into the **host** app:
171
+
172
+ - Cursor: `node_modules/panelize/examples/cursor-rule.mdc` → `.cursor/rules/panelize.mdc` (`alwaysApply: true`).
173
+ - Any agent (`AGENTS.md`, Copilot instructions, and so on): `node_modules/panelize/examples/AGENTS.md`.
174
+
175
+ Copyable layouts (page, sidebar, centered card, form, dashboard grid): [`docs/recipes.md`](docs/recipes.md). One line per export: [`docs/components.md`](docs/components.md).
176
+
177
+ **Changing this library:** [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`AGENTS.md`](AGENTS.md).
178
+
179
+ ## Theme
180
+
181
+ Export a **stable** object and wrap the tree once. Omit any key to keep the library default.
182
+
183
+ ```ts
184
+ import { createPanelizeTheme } from 'panelize'
185
+
186
+ export const panelizeTheme = createPanelizeTheme({
187
+ colors: {
188
+ dark: {
189
+ app: { background: '#121212', text: '#e8e8e8' },
190
+ muted: {
191
+ background: '#222222',
192
+ text: { base: '#fafafa', muted: '#9a9a9a', highlighted: '#ffffff' },
193
+ },
194
+ },
195
+ // Omit a scheme or variant to keep the library default.
196
+ },
197
+ brand: { primary: '#ff9f2e' },
198
+ })
199
+ ```
200
+
201
+ ```tsx
202
+ <PanelizeProvider theme={panelizeTheme} mode={mode}>
203
+ <Panel variant='muted'>
204
+ <Text>Uses your muted background and text</Text>
205
+ </Panel>
206
+ </PanelizeProvider>
207
+ ```
208
+
209
+ Without a provider, panels still render using `defaultTheme` (`mode` `dark`).
210
+
211
+ Author schemes as `createPanelizeTheme({ colors: { dark, light } })`. After create, **`theme.colors` is the active slice** (`muted` / `base` / `highlighted` / `app`) — not `{ dark, light }`. Both maps are `theme.colorSchemes`. `PanelizeProvider` `mode` (default `dark`) picks which slice is active. `usePanelizeTheme().colors` is that slice; `.mode` is the name. Surfaces belong **under** a scheme (`colors.dark.muted`). A top-level `colors.muted` is ignored. `dark` is a scheme, not a panel variant. `borderColor` is a surface (`muted` / `base` / `highlighted`), brand (`primary`), status (`danger`), or raw CSS. Brand and status are shared. Hosts own toggle state — there is no library toggle. `prefers-color-scheme` is host-side, not a `mode` value. The returned object from `createPanelizeTheme` always has `mode: 'dark'`; the provider prop is what the tree uses.
212
+
213
+ Each panel variant has `background`, `border`, and `text.base` / `text.muted` / `text.highlighted`. The panel’s CSS `color` is `text.base` (`--panelize-text`). Brand (`primary` / `secondary` / `accent`) and status (`danger` / `warning` / `success` / `info`) are global, shared across schemes. Each slot is `{ base, hover, active }`. A hex fills all three (hover lighter, active darker). `Text` / `Button` `color="primary"` uses **base**; `color="danger"` uses `status.danger.base`. Hover/active are for `TextLink` and `Button`. Library `brand.secondary` / `accent` follow `primary` until the host sets them. Status slots do not follow a primary. Filled `Button` ink stays `palette.black`. There is no `subtle` — `Chip` already mixes chroma into `--panelize-bg`. Warning `#ffd60a` as small type on a light panel is weak; filled Button and Chip tint are the intended uses.
214
+
215
+ `colors.app` is optional per scheme. `PanelizeProvider` always wraps the tree (CSS `color-scheme` from `mode`). If you pass `app.background` and/or `app.text`, those become `--panelize-app-bg` / `--panelize-app-text` on that wrapper. It does not set `html` / `body`.
216
+
217
+ ```tsx
218
+ <Panel variant='muted'>
219
+ <Text>Muted</Text>
220
+ </Panel>
221
+ <Panel variant='base'>
222
+ <Text>Base</Text>
223
+ </Panel>
224
+ <Panel variant='highlighted'>
225
+ <Text>Highlighted</Text>
226
+ </Panel>
227
+ ```
228
+
229
+ CSS variables on the panel: `--panelize-bg`, `--panelize-text`, `--panelize-text-muted`, `--panelize-text-highlighted`. Nested panels and non-Panel children share them.
230
+
231
+ ## Color bank
232
+
233
+ One export: `palette`. Not a theme key — cite it when you author a theme or pass `color={palette.gray.dark}` on `Text`. Hue ramps (`palette.red`, …) will use the same five **names** later.
234
+
235
+ `palette.gray[0]`–`[6]` is a 7-step even-L\* ramp (black → white). Names alias those steps. `palette.black` / `palette.white` are `[0]` / `[6]`. Adjacent steps are **not** a text-on-fill pair — skip at least one.
236
+
237
+ | Token | Hex | Alias |
238
+ | ----------------- | --------- | --------------- |
239
+ | `palette.gray[0]` | `#000000` | `palette.black` |
240
+ | `palette.gray[1]` | `#292929` | `extraDark` |
241
+ | `palette.gray[2]` | `#4e4e4e` | `dark` |
242
+ | `palette.gray[3]` | `#777777` | `base` |
243
+ | `palette.gray[4]` | `#a2a2a2` | `light` |
244
+ | `palette.gray[5]` | `#d0d0d0` | `extraLight` |
245
+ | `palette.gray[6]` | `#ffffff` | `palette.white` |
246
+
247
+ ```ts
248
+ import { createPanelizeTheme, palette } from 'panelize'
249
+
250
+ createPanelizeTheme({
251
+ colors: {
252
+ dark: {
253
+ muted: {
254
+ background: palette.gray.extraDark,
255
+ text: { base: palette.white, muted: palette.gray.base, highlighted: palette.white },
256
+ border: palette.gray.dark,
257
+ },
258
+ },
259
+ },
260
+ })
261
+ ```
262
+
263
+ ## Layout
264
+
265
+ `fill` sizes to the parent. On default (absolute) `Panels` that is `inset: 0`, not `height` / `width` 100% — percentage fill on an overlay creates a page scrollbar. Everywhere else (`relative` `Panels`, `Panel`, `Rows`, `Cols`, `Grid`, `Div`) `fill` is 100%. `Row` / `Col` also take `fill`: theme `height` / `width` `auto` counts as a size, so `Row fill` is width 100% (height stays auto) and `Col fill` is height 100% (width stays auto) unless you set that axis. Layout boxes are `border-box`, so padding sits inside `fill`. Explicit `width` / `height` override `fill` on that axis. `flex` grows in a flex parent (`flex: 1 1 0%`) and can shrink below content (`min-width` / `min-height` 0) so a nested scrolling pane works. That is not `fill`. `Panel`, `Row`, `Col`, and `Div` are `flex-shrink: 0` until `flex`.
266
+
267
+ `Panels` defaults to a **column** (`absolute`; `relative` opt-in). `horizontal` is a row (sidebar + main). `Panel`, `Rows`, `Cols`, `Grid`, and `Div` stay `relative`. Overlay shells belong on `Panels`, not `Rows fill`.
268
+
269
+ A filled shell needs a viewport-tall host (`html` / `body` / `#root { height: 100% }`). For a long in-flow page, drop `height` and keep `min-height: 100%`.
270
+
271
+ Page (scroll the shell). Default column — do not add `horizontal`. Inner `fill` locks height and the surface will not grow; `minHeight="100%"` fills a short window. `Panel` / `Row` / `Col` / `Div` do not shrink in a flex parent unless `flex`. Center on inner `Rows`, not on the scrolling `Panels`.
272
+
273
+ ```tsx
274
+ <Panels fill scroll='vertical'>
275
+ <Panel variant='muted' minHeight='100%'>
276
+ <Heading level={1}>Title</Heading>
277
+ <Text muted>Page copy…</Text>
278
+ </Panel>
279
+ </Panels>
280
+ ```
281
+
282
+ Split (scroll the **pane**, not the outer `Panels`). Omit `horizontal` and the panes stack. `stack` is for this row: at most this wide → column. `fill` on the main pane fights the sidebar (`flex` instead).
283
+
284
+ ```tsx
285
+ <Panels horizontal fill stack='sm'>
286
+ <Panel variant='muted' width='12rem'>
287
+ <Text>Nav</Text>
288
+ </Panel>
289
+ <Panel variant='base' flex scroll='vertical'>
290
+ <Text>Main</Text>
291
+ </Panel>
292
+ </Panels>
293
+ ```
294
+
295
+ `scroll` is only on `Panels` and `Panel`. `scroll="vertical"` clips x (`overflow-x: hidden`). A horizontal bar is `scroll="horizontal"` / `"both"`, and only if a child is actually wider than the box.
296
+
297
+ `gap` is on `Panels`, `Rows`, `Cols`, and `Grid`. `fill` is on those plus `Panel`, `Row`, `Col`, and `Div` — not on `Cell`. `width` / `height` are on the same set. Omit means unset, except `Row` height and `Col` width default to theme `auto`. Size limits (`minWidth`, …) are on every layout component including `Cell`.
298
+
299
+ `Div` is a catch-all box (spacing, size, border, `fill` / `flex`). It does not set `background` or `color`.
300
+
301
+ `alignX` / `alignY` are **screen** axes. Track omit: flex cross `stretch` / main `start`; `Grid` `stretch` / `stretch`. Child omit: inherit. Unused flex axis is a no-op. `space-*` is flex main; `baseline` is flex cross.
302
+
303
+ `wrap` is on `Rows` / `Cols` (not `Grid`). `stack` is on `horizontal` `Panels` / `Cols` / `Grid`: **at most** this wide → column. Not Tailwind `md:` (and-up). Default `Panels` is already a column (`stack` there is ignored). `stackCols` is column count while stacked (default 1). `spanX` / `spanY` are on `Cell`; `stack` forces `spanX` to 1.
304
+
305
+ ```tsx
306
+ <Panels fill alignX='center' alignY='center'>
307
+ <Panel>
308
+ <Text>Centered</Text>
309
+ </Panel>
310
+ </Panels>
311
+ <Panels horizontal fill stack='sm'>
312
+ <Panel>
313
+ <Text>Nav</Text>
314
+ </Panel>
315
+ <Panel flex>
316
+ <Text>Main</Text>
317
+ </Panel>
318
+ </Panels>
319
+ <Rows wrap gap='small' alignX='center' alignY='space-between'>
320
+ <Row height='4rem'>
321
+ <Text>One</Text>
322
+ </Row>
323
+ <Row height='4rem'>
324
+ <Text>Two</Text>
325
+ </Row>
326
+ </Rows>
327
+ <Cols stack='sm' gap='small'>
328
+ <Col width='12rem'>
329
+ <Text>Side</Text>
330
+ </Col>
331
+ <Col flex>
332
+ <Text>Main</Text>
333
+ </Col>
334
+ </Cols>
335
+ <Grid numCols={3} stackCols={2} gap='small' stack='sm'>
336
+ <Cell>
337
+ <Text>A</Text>
338
+ </Cell>
339
+ <Cell spanX={2}>
340
+ <Text>B</Text>
341
+ </Cell>
342
+ </Grid>
343
+ ```
344
+
345
+ Color/background inherit from the parent `Panel`. Directional borders are on every layout box, including tracks (`Panels` / `Rows` / `Cols` / `Grid`).
346
+
347
+ ## Space
348
+
349
+ One rem scale for padding, margin, and gap. `Button` uses the same tokens for `padding` / `paddingX` / `paddingY` (no margin — use a layout parent).
350
+
351
+ | Token | Default |
352
+ | ------------ | --------- |
353
+ | `none` | `0rem` |
354
+ | `extraSmall` | `0.25rem` |
355
+ | `small` | `0.5rem` |
356
+ | `base` | `1rem` |
357
+ | `large` | `1.5rem` |
358
+ | `extraLarge` | `2rem` |
359
+
360
+ Library default is `none` when no prop is set. Token or raw CSS length — not a unitless number. Per-side order: `padding` (or theme) → `paddingX` / `paddingY` → `paddingTop` / … (same for margin on layout).
361
+
362
+ ```tsx
363
+ <Panel padding="small" width="12rem" />
364
+ <Panel paddingX="small" paddingTop="1rem" />
365
+ <Button paddingX="small" paddingY="extraSmall" label="Compact" />
366
+ ```
367
+
368
+ ## Borders
369
+
370
+ Same token **names** as space. Width is **px**. Radius is **rem**. Library default is `none`. Color comes from the variant (`usePanelizeTheme().colors.muted.border`). Override with `borderColor`: panel surface (`muted` / `base` / `highlighted`), brand (`primary`), status (`danger`), or CSS. `base` is the panel `base` border, not a brand slot. `dark` / `light` are schemes, not border colors.
371
+
372
+ Every layout box (`Panels` / `Panel` / `Rows` / `Row` / `Cols` / `Col` / `Grid` / `Cell` / `Div`) takes the same width cascade as padding: `borderWidth` → `borderX` / `borderY` → `borderTop` / `borderRight` / `borderBottom` / `borderLeft`. One `borderColor` paints every side that has width. A border is box chrome, not a surface — `variant` / fill stay on `Panel`. Default width is `none`.
373
+
374
+ | Token | Width | Radius |
375
+ | ------------ | ------ | ---------- |
376
+ | `none` | `0` | `0` |
377
+ | `extraSmall` | `1px` | `0.125rem` |
378
+ | `small` | `2px` | `0.25rem` |
379
+ | `base` | `4px` | `0.5rem` |
380
+ | `large` | `8px` | `0.75rem` |
381
+ | `extraLarge` | `12px` | `1rem` |
382
+
383
+ ```tsx
384
+ <Panel variant='muted' borderWidth='extraSmall' borderRadius='small' />
385
+ <Panel borderWidth='extraSmall' borderColor='highlighted' />
386
+ <Rows>
387
+ <Row borderBottom='extraSmall' borderColor='primary'>
388
+ <Text>Section</Text>
389
+ </Row>
390
+ <Row>
391
+ <Text>Next</Text>
392
+ </Row>
393
+ </Rows>
394
+ ```
395
+
396
+ ## Type
397
+
398
+ `Text` is a `<p>`. `Heading` is `h1`–`h6` (`level` required). `Span` is a `<span>`. `TextLink` is an `<a>`. None is a layout shell. Size/weight are **type** tokens, not space.
399
+
400
+ Color from the nearest `Panel`: omit = `text.base`; `muted` / `highlighted`; `color="primary"` (or `secondary` / `accent`) = brand **base**; `color="danger"` (or `warning` / `success` / `info`) = status **base**; raw CSS on `color` wins. If both flags, `highlighted` wins.
401
+
402
+ `Span` nested in `Text` / `Span`: omit size/weight/`family` → inherit that line; omit color → CSS `inherit`. Standalone omit size/weight/`family` → `defaults.text` and `base`.
403
+
404
+ `TextLink` default rest/hover/active is `brand.primary`. Nested omit size/weight/`family` → inherit. `hoverUnderline` default `false`. `:focus-visible` is `currentColor`. `defaults.textLink.hover: 'inherit'` uses panel `text.base`.
405
+
406
+ `Heading` `level` picks the tag. Omit `size` = `defaults.heading.size[level]` (UA rem: `2` / `1.5` / `1.17` / `1` / `0.83` / `0.67`). Instance `size` overrides. Omit `weight` = `bold`. Omit `family` = `display`. Hosts change the bank in `createPanelizeTheme`. No UA heading margin; the line box hugs the glyphs (`line-height: 1`). Put gap on a `Row` / `Col`, not on `Heading`.
407
+
408
+ Family is a token prop: `base` (Open Sans), `display` (Montserrat), `mono` (system stack). Not CSS `display`. `Text` / `Button` / `Chip` omit = `base`. The library does not load font files. `lineHeight` is unitless `1.2`. `uppercase` is a bool. Omit `weight` on `Text` = `base` (`400`) so parent weight does not leak.
409
+
410
+ | `size` | Default | `weight` | CSS |
411
+ | ------------ | ---------- | -------- | --- |
412
+ | `extraSmall` | `0.75rem` | `thin` | 100 |
413
+ | `small` | `0.875rem` | `base` | 400 |
414
+ | `base` | `1rem` | `bold` | 700 |
415
+ | `large` | `1.25rem` | `heavy` | 800 |
416
+ | `extraLarge` | `1.5rem` | | |
417
+
418
+ ```tsx
419
+ <Heading level={1}>
420
+ Title
421
+ </Heading>
422
+ <Text family='display' size='large' weight='bold' uppercase>
423
+ Display
424
+ </Text>
425
+ <Text muted size='small'>
426
+ Nested <Span weight='bold'>bold</Span> and <TextLink href='/docs'>Docs</TextLink>
427
+ </Text>
428
+ ```
429
+
430
+ ## Controls
431
+
432
+ Forms, buttons, and chips are bonus. Each takes `color`, `style`, and `css`; the wrapper places them. Full behavior notes: [`docs/controls.md`](docs/controls.md). Value ownership and validation: [`docs/forms.md`](docs/forms.md).
433
+
434
+ ### Button
435
+
436
+ `<button>` with a filled brand slot by default; `outline` for a hollow one. Caption via `label`. Details: [`docs/controls.md`](docs/controls.md#button).
437
+
438
+ ```tsx
439
+ <Button label='Deploy now' />
440
+ <Button outline label='Details' />
441
+ <Button color='secondary' label='Secondary' />
442
+ <Button color='danger' outline label='Delete' />
443
+ <Button fill label='Stretch' />
444
+ <Button disabled label='Disabled' />
445
+ <Button css='letter-spacing: 0.08em' label='Custom css' />
446
+ <Button background='highlighted' ink='primary' label='Custom paint' />
447
+ <Button
448
+ outline
449
+ ink={{ base: 'primary', hover: 'highlighted', active: 'highlighted' }}
450
+ label='Ghost'
451
+ />
452
+ ```
453
+
454
+ ### Chip
455
+
456
+ `<span>` badge tinted from a brand or status slot. Not a button. Details: [`docs/controls.md`](docs/controls.md#chip).
457
+
458
+ ```tsx
459
+ <Chip label='New' />
460
+ <Chip color='secondary' label='Beta' />
461
+ <Chip color='success' label='Live' />
462
+ <Chip borderWidth='extraSmall' borderColor='accent' label='Stroke' />
463
+ <Chip background='highlighted' ink='primary' label='Painted' />
464
+ ```
465
+
466
+ ### Input
467
+
468
+ Native `<input>`. Rest chrome comes from the nearest `Panel`; `color` is the **focus** accent only. Details: [`docs/controls.md`](docs/controls.md#input).
469
+
470
+ ```tsx
471
+ <Input placeholder='Focus me' aria-label='Focus me' fill />
472
+ <Input color='accent' placeholder='Accent focus' aria-label='Accent focus' />
473
+ <Input background='highlighted' ink='primary' borderRadius='extraSmall' aria-label='Painted' />
474
+ <Input borderColor='primary' aria-label='Primary rest border' />
475
+ <Input disabled placeholder='Disabled' aria-label='Disabled' />
476
+ ```
477
+
478
+ ### NumberInput
479
+
480
+ `Input` locked to `type="number"`. Read `event.currentTarget.valueAsNumber`. Details: [`docs/controls.md`](docs/controls.md#numberinput).
481
+
482
+ ```tsx
483
+ <Label htmlFor='count' label='Count' />
484
+ <NumberInput id='count' min={0} max={100} step={1} defaultValue={1} fill />
485
+ <NumberInput min={0} step='any' placeholder='Amount' aria-label='Amount' />
486
+ ```
487
+
488
+ ### Textarea
489
+
490
+ Same chrome as `Input`, as a `<textarea>`. Resize is off until `resize`. Details: [`docs/controls.md`](docs/controls.md#textarea).
491
+
492
+ ```tsx
493
+ <Textarea placeholder='Notes' aria-label='Notes' fill />
494
+ <Textarea resize placeholder='Drag me' aria-label='Drag me' />
495
+ <Textarea color='secondary' rows={6} placeholder='Longer' aria-label='Longer' />
496
+ ```
497
+
498
+ ### Select
499
+
500
+ Custom listbox (not a native `<select>`) with a portal menu, `clearable`, `required`, `emptyOption`, and keyboard support. Details: [`docs/controls.md`](docs/controls.md#select).
501
+
502
+ ```tsx
503
+ <Label htmlFor='country' label='Country' />
504
+ <Select
505
+ id='country'
506
+ placeholder='Select country'
507
+ value={country}
508
+ onChange={setCountry}
509
+ clearable
510
+ selectedAtTop
511
+ emptyOption
512
+ fill
513
+ >
514
+ <Option value='us'>United States</Option>
515
+ <Option value='mx'>Mexico</Option>
516
+ </Select>
517
+ ```
518
+
519
+ ### MultiSelect
520
+
521
+ Custom multi listbox. Shows `N selected`; the menu stays open while toggling. Details: [`docs/controls.md`](docs/controls.md#multiselect).
522
+
523
+ ```tsx
524
+ <Label htmlFor='countries' label='Countries' />
525
+ <MultiSelect
526
+ id='countries'
527
+ placeholder='Select countries'
528
+ value={countries}
529
+ onChange={setCountries}
530
+ clearable
531
+ selectedAtTop
532
+ fill
533
+ >
534
+ <Option value='us'>United States</Option>
535
+ <Option value='ca'>Canada</Option>
536
+ <Option value='jp'>Japan</Option>
537
+ </MultiSelect>
538
+ ```
539
+
540
+ ### Switch
541
+
542
+ `<input role="switch">`. Wrap it in a `Label` of the same `size`. Details: [`docs/controls.md`](docs/controls.md#switch).
543
+
544
+ ```tsx
545
+ <Label htmlFor='auto-save' label='Auto-save drafts' />
546
+ <Switch
547
+ id='auto-save'
548
+ defaultChecked
549
+ onChange={(event) => setAutoSave(event.currentTarget.checked)}
550
+ />
551
+ <Switch color='secondary' defaultChecked aria-label='Secondary' />
552
+ <Switch size='large' color='accent' borderRadius='small' aria-label='Accent' />
553
+ <Switch color='success' defaultChecked aria-label='Live' />
554
+ <Switch disabled aria-label='Off and disabled' />
555
+ ```
556
+
557
+ ### Checkbox
558
+
559
+ `<input type="checkbox">` with `indeterminate` and `hideCheckIcon`. Wrap it in a `Label` of the same `size`. Details: [`docs/controls.md`](docs/controls.md#checkbox).
560
+
561
+ ```tsx
562
+ <Label htmlFor='sms' label='SMS notifications' />
563
+ <Checkbox
564
+ id='sms'
565
+ defaultChecked
566
+ onChange={(event) => setSms(event.currentTarget.checked)}
567
+ />
568
+ <Checkbox color='secondary' defaultChecked aria-label='Secondary' />
569
+ <Checkbox color='accent' aria-label='Accent' />
570
+ <Checkbox indeterminate aria-label='Select all' />
571
+ <Checkbox hideCheckIcon defaultChecked aria-label='Fill only' />
572
+ <Checkbox disabled aria-label='Off and disabled' />
573
+ <Checkbox defaultChecked disabled aria-label='On and disabled' />
574
+ ```
575
+
576
+ ### Radio
577
+
578
+ `<input type="radio">`; the host owns the group. Wrap it in a `Label` of the same `size`. Details: [`docs/controls.md`](docs/controls.md#radio).
579
+
580
+ ```tsx
581
+ <Label htmlFor='hobby' size='small'>
582
+ <Radio
583
+ id='hobby'
584
+ name='plan'
585
+ value='hobby'
586
+ size='small'
587
+ defaultChecked
588
+ onChange={(event) => setPlan(event.currentTarget.value)}
589
+ />{' '}
590
+ Hobby — Free
591
+ </Label>
592
+ <Radio name='plan' value='pro' color='secondary' aria-label='Pro' />
593
+ <Radio name='plan' value='team' color='accent' aria-label='Team' />
594
+ <Radio name='plan' value='enterprise' disabled aria-label='Enterprise' />
595
+ ```
596
+
597
+ ### Label
598
+
599
+ `<label>` with `Text`-like ink. Wrap `Switch` / `Checkbox` / `Radio` in it. Details: [`docs/controls.md`](docs/controls.md#label).
600
+
601
+ ```tsx
602
+ <Label htmlFor='email' label='Email' />
603
+ <Input id='email' type='email' />
604
+ <Label htmlFor='sms' size='small'>
605
+ <Checkbox id='sms' size='small' /> SMS notifications
606
+ </Label>
607
+ <Label htmlFor='docs'>
608
+ Read the <TextLink href='/docs'>docs</TextLink>
609
+ </Label>
610
+ ```
611
+
612
+ ## Contributing
613
+
614
+ Humans and agents changing this library: [`CONTRIBUTING.md`](CONTRIBUTING.md).
615
+
616
+ ## License
617
+
618
+ [MIT](LICENSE)