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/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +617 -2
- package/dist/index.d.cts +1815 -0
- package/dist/index.d.ts +1815 -0
- package/dist/panelize.cjs +858 -0
- package/dist/panelize.cjs.map +1 -0
- package/dist/panelize.es.js +3011 -0
- package/dist/panelize.es.js.map +1 -0
- package/docs/accessibility.md +43 -0
- package/docs/components.md +61 -0
- package/docs/composition.md +138 -0
- package/docs/controls.md +47 -0
- package/docs/forms.md +64 -0
- package/docs/layout.md +59 -0
- package/docs/recipes.md +170 -0
- package/docs/shell.md +57 -0
- package/docs/text.md +26 -0
- package/docs/theme.md +121 -0
- package/examples/AGENTS.md +9 -0
- package/examples/cursor-rule.mdc +17 -0
- package/examples/panelize.theme.md +312 -0
- package/llms-full.txt +1206 -0
- package/llms.txt +51 -0
- package/package.json +105 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
What Panelize does for you, and what the host must still do.
|
|
4
|
+
|
|
5
|
+
## Names
|
|
6
|
+
|
|
7
|
+
Every control needs an accessible name. Use a `Label` with `htmlFor` matching the control `id`, or wrap the control in a `Label`. With no visible text, use `aria-label`. A `placeholder` is not a name.
|
|
8
|
+
|
|
9
|
+
- `Input` / `NumberInput` / `Textarea` / `Select` / `MultiSelect`: `Label htmlFor` + `id`, else `aria-label`.
|
|
10
|
+
- `Switch` / `Checkbox` / `Radio`: wrap in a `Label` (same `size`). These have no `label` prop.
|
|
11
|
+
- `Button`: `label`, or `children` with text. Icon-only: `aria-label`.
|
|
12
|
+
- `Chip` is not interactive. Do not use it as a button.
|
|
13
|
+
|
|
14
|
+
## Structure
|
|
15
|
+
|
|
16
|
+
- `Heading` needs `level` and renders `h1`–`h6`. Pick the level for the document outline, not for size (`size` is separate).
|
|
17
|
+
- `Panels` / `Panel` / `Rows` / `Cols` / `Grid` are plain `div`s. They add no landmarks or roles. Wrap them in `header` / `nav` / `main` / `section` in the host.
|
|
18
|
+
- Copy goes in `Text` / `Heading` / `Span` / `TextLink` / `Label` (real `p` / `h*` / `span` / `a` / `label`).
|
|
19
|
+
|
|
20
|
+
## Keyboard
|
|
21
|
+
|
|
22
|
+
| Control | Keys |
|
|
23
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `Select` | Arrows move, Enter / Space pick, Home / End, type-ahead, Escape closes, Backspace / Delete clears when `clearable`, Tab leaves and closes |
|
|
25
|
+
| `MultiSelect` | Same, but Enter / Space toggle. Type-ahead on a closed field opens and highlights |
|
|
26
|
+
| `Switch` / `Checkbox` | Space toggles |
|
|
27
|
+
| `Radio` | Arrow keys move within a `name` group |
|
|
28
|
+
| `Button` / `TextLink` | Native |
|
|
29
|
+
|
|
30
|
+
Disabled options are skipped. Escape is handled on the trigger, not on `window`.
|
|
31
|
+
|
|
32
|
+
## Roles and state
|
|
33
|
+
|
|
34
|
+
- `Select` / `MultiSelect`: trigger is `role='combobox'` with `aria-haspopup='listbox'`, `aria-expanded`, and `aria-autocomplete='none'`. The menu is `role='listbox'` with `role='option'` rows. The listbox is labelled by the control only when you set `id`. `readOnly` sets `aria-readonly`.
|
|
35
|
+
- `Switch` is `input[type=checkbox][role=switch]`.
|
|
36
|
+
- Invalid fields: set `aria-invalid='true'`. The library paints it and `:user-invalid`. Link the error text with `aria-describedby`.
|
|
37
|
+
|
|
38
|
+
## Focus and contrast
|
|
39
|
+
|
|
40
|
+
- Focus is `:focus-visible` only: fields ring and glow in their `color`, `TextLink` uses a `currentColor` outline, switches / checkboxes / radios glow in the on-state chroma.
|
|
41
|
+
- `disabled` controls dim and are not focusable. `readOnly` on `Select` / `MultiSelect` stays full contrast.
|
|
42
|
+
- Contrast is the host’s job. The default theme is dark first. `palette` adjacent steps are **not** a safe text-on-fill pair; skip a step. Default `warning` (`#ffd60a`) is weak as small text on light panels. Check any custom `createPanelizeTheme` colors.
|
|
43
|
+
- `PanelizeProvider` sets CSS `color-scheme` from `mode` so native scrollbars and form chrome match. Respecting `prefers-color-scheme` or `prefers-reduced-motion` is host-side.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
One line per export from `'panelize'`. Prop details are in the exported prop types (IDE hover) and the [README](../README.md). Layout props are in [layout](layout.md). Every component takes React `style` and `css?: string` (appended last).
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
| Component | Element | Direct parent | Notes |
|
|
8
|
+
| --------- | -------------- | ------------- | ---------------------------------------------------------------------------------------- |
|
|
9
|
+
| `Panels` | `div` | any | View shell. Children: only `Panel`. `fill`, `horizontal`, `relative`, `scroll`, `stack`. |
|
|
10
|
+
| `Panel` | `div` | `Panels` | Section. `variant` `muted` / `base` / `highlighted`. Publishes `--panelize-*`. |
|
|
11
|
+
| `Rows` | `div` (column) | any | Children: two or more `Row`. `gap`, `wrap`, `alignX` / `alignY`. |
|
|
12
|
+
| `Row` | `div` | `Rows` | `height` defaults to `auto`. |
|
|
13
|
+
| `Cols` | `div` (row) | any | Children: two or more `Col`. `gap`, `wrap`, `stack`. |
|
|
14
|
+
| `Col` | `div` | `Cols` | `width` defaults to `auto`. |
|
|
15
|
+
| `Grid` | `div` (grid) | any | Children: two or more `Cell`. `numCols`, `numRows`, `stack`, `stackCols`. |
|
|
16
|
+
| `Cell` | `div` | `Grid` | `spanX` / `spanY`. No `width` / `height` / `fill`. |
|
|
17
|
+
| `Div` | `div` | any | Catch-all wrapper. No background or color. |
|
|
18
|
+
|
|
19
|
+
## Type
|
|
20
|
+
|
|
21
|
+
| Component | Element | Notes |
|
|
22
|
+
| ---------- | --------- | --------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `Text` | `p` | `muted` / `highlighted` / `color` / `size` / `weight` / `family` / `italic` / `underlined` / `uppercase`. |
|
|
24
|
+
| `Heading` | `h1`–`h6` | Required `level`. Same type props as `Text`. |
|
|
25
|
+
| `Span` | `span` | Inline. Inherits size / weight / family from a parent `Text` / `Span`. |
|
|
26
|
+
| `TextLink` | `a` | `color`, `hover`, `hoverUnderline`. Native `href`. |
|
|
27
|
+
| `Label` | `label` | `label` or `children`, `htmlFor`. Wrap `Switch` / `Checkbox` / `Radio`. |
|
|
28
|
+
|
|
29
|
+
`color` on type and controls: `primary` / `secondary` / `accent` (brand), `danger` / `warning` / `success` / `info` (status), or raw CSS.
|
|
30
|
+
|
|
31
|
+
## Controls
|
|
32
|
+
|
|
33
|
+
None is a layout shell. The wrapper (`Row` / `Col` / `Cell` / `Panel` / `Div`) places them.
|
|
34
|
+
|
|
35
|
+
| Component | Element | Value API | Notes |
|
|
36
|
+
| ------------- | ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------------- |
|
|
37
|
+
| `Button` | `button` | `onClick` | `label` or `children`. `color`, `outline`, `size`, `fill`, `disabled`. |
|
|
38
|
+
| `Chip` | `span` | none | Badge, not a button. `label`, `color`. |
|
|
39
|
+
| `Input` | `input` | `value` / `onChange` | `color` is focus chroma. `ink` is text. Needs `Label` or `aria-label`. |
|
|
40
|
+
| `NumberInput` | `input[type=number]` | `value` / `onChange` (string) | Use `event.currentTarget.valueAsNumber`. |
|
|
41
|
+
| `Textarea` | `textarea` | `value` / `onChange` | `resize` opt-in, `rows` default 3. |
|
|
42
|
+
| `Select` | custom listbox | `value: string` / `onChange(value)` | `options` and/or `Option` children. `clearable`, `required`, `emptyOption`. |
|
|
43
|
+
| `MultiSelect` | custom multi listbox | `value: string[]` / `onChange(value)` | Count caption. Menu stays open while toggling. |
|
|
44
|
+
| `Option` | child of selects | `value` | Label is `children`. |
|
|
45
|
+
| `Switch` | `input[checkbox][role=switch]` | `checked` / `onChange` | `color` is the on track. |
|
|
46
|
+
| `Checkbox` | `input[checkbox]` | `checked` / `onChange` | `indeterminate`, `hideCheckIcon`. |
|
|
47
|
+
| `Radio` | `input[radio]` | `name` / `value` / `checked` / `onChange` | Host owns the group. |
|
|
48
|
+
|
|
49
|
+
`Switch` / `Checkbox` / `Radio` and their `Label` share one `size`. These three have no `fill`, `label`, or `children`.
|
|
50
|
+
|
|
51
|
+
## Theme
|
|
52
|
+
|
|
53
|
+
| Export | Purpose |
|
|
54
|
+
| -------------------------------- | ------------------------------------------------------------------------------------- |
|
|
55
|
+
| `createPanelizeTheme(overrides)` | Merge overrides onto the defaults. Keys: [theme keys](../examples/panelize.theme.md). |
|
|
56
|
+
| `PanelizeProvider` | `theme` (optional) and `mode` `dark` (default) / `light`. |
|
|
57
|
+
| `usePanelizeTheme()` | Active `colors`, `mode`, `brand`, `status`, scales, `defaults`. |
|
|
58
|
+
| `defaultTheme` | What components use without a provider. |
|
|
59
|
+
| `palette` | Frozen gray ramp: `gray[0]`–`[6]`, `black`, `white`, `extraDark` … `extraLight`. |
|
|
60
|
+
|
|
61
|
+
Tokens: space / border / radius `none` | `extraSmall` | `small` | `base` | `large` | `extraLarge`. Font size `extraSmall` … `extraLarge`. Weight `thin` | `base` | `bold` | `heavy`. Family `base` | `display` | `mono`. Breakpoints `sm` | `md` | `lg`.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Composition
|
|
2
|
+
|
|
3
|
+
Build views and custom components from layout primitives. The JSX should read as the picture without opening the browser.
|
|
4
|
+
|
|
5
|
+
## Pairing (bidirectional)
|
|
6
|
+
|
|
7
|
+
| Item | Direct parent | That parent’s direct children |
|
|
8
|
+
| ------- | ------------- | ----------------------------- |
|
|
9
|
+
| `Panel` | `Panels` | only `Panel` (one is allowed) |
|
|
10
|
+
| `Row` | `Rows` | only `Row`, **two or more** |
|
|
11
|
+
| `Col` | `Cols` | only `Col`, **two or more** |
|
|
12
|
+
| `Cell` | `Grid` | only `Cell`, **two or more** |
|
|
13
|
+
|
|
14
|
+
`Row` must not sit on `Panel`, `Div`, `Col`, `Cell`, or `Grid`. Same idea for `Col` and `Cell`. Fragments and `.map` still have to produce those children.
|
|
15
|
+
|
|
16
|
+
Custom components are **not** substitutes for `Row` / `Col` / `Cell`. Build them from primitives, then place the custom component inside a `Row`, `Col`, `Cell`, `Panel`, or `Div`.
|
|
17
|
+
|
|
18
|
+
## Shell → sections → tracks
|
|
19
|
+
|
|
20
|
+
1. `Panels` — outer shell of the view (`fill`, `scroll`, column vs `horizontal`).
|
|
21
|
+
2. `Panel` — sections (surfaces). One is fine. Several for nav + main, or stacked regions.
|
|
22
|
+
3. Inside a `Panel`: `Rows` / `Cols` / `Grid` / `Div` / nested `Panels` / type / controls / custom components.
|
|
23
|
+
|
|
24
|
+
A `Panel` is an **open** section. It is not a homogeneous track.
|
|
25
|
+
|
|
26
|
+
If there is only one band of content, do not wrap it in `Rows` of one `Row` (or `Cols` of one `Col`, or `Grid` of one `Cell`). Put that content in the `Panel` or a `Div`.
|
|
27
|
+
|
|
28
|
+
## Who positions
|
|
29
|
+
|
|
30
|
+
The wrapper layout box owns place. Set `alignX` / `alignY`, `flex`, `fill`, `gap`, `width` / `height` on `Panels`, `Panel`, `Rows`, `Row`, `Cols`, `Col`, `Grid`, `Cell`, or `Div` — not on `Text`, `Button`, or a custom widget so it can “nudge itself.”
|
|
31
|
+
|
|
32
|
+
`Text` / `Heading` / `Span` / `TextLink` / `Label` are not layout shells (no `fill` / `flex` / padding). Controls may take `fill` to stretch the control itself; the parent still decides where the control sits.
|
|
33
|
+
|
|
34
|
+
## Type
|
|
35
|
+
|
|
36
|
+
Visible writing uses `Text`, `Heading`, `Span`, `TextLink`, or `Label`. Bare strings in a `Panel` / `Row` / `Col` / `Div` do not pick up `--panelize-text` / muted / highlighted.
|
|
37
|
+
|
|
38
|
+
## Good
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<Panels fill scroll='vertical'>
|
|
42
|
+
<Panel variant='muted' minHeight='100%'>
|
|
43
|
+
<Rows minHeight='100%' gap='base' alignX='center' alignY='center'>
|
|
44
|
+
<Row>
|
|
45
|
+
<Heading level={1} color='primary'>
|
|
46
|
+
Panelize
|
|
47
|
+
</Heading>
|
|
48
|
+
</Row>
|
|
49
|
+
<Row>
|
|
50
|
+
<Text muted>A React panel layout library.</Text>
|
|
51
|
+
</Row>
|
|
52
|
+
<Row>
|
|
53
|
+
<Cols gap='small'>
|
|
54
|
+
<Col>
|
|
55
|
+
<Button label='Deploy now' />
|
|
56
|
+
</Col>
|
|
57
|
+
<Col>
|
|
58
|
+
<Button outline label='Details' />
|
|
59
|
+
</Col>
|
|
60
|
+
</Cols>
|
|
61
|
+
</Row>
|
|
62
|
+
</Rows>
|
|
63
|
+
</Panel>
|
|
64
|
+
</Panels>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Column of rows, last row is two columns of buttons. The `Cols` gap and the `Row` place the buttons; the buttons do not.
|
|
68
|
+
|
|
69
|
+
One section, one heading — no `Rows`:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Panels fill>
|
|
73
|
+
<Panel variant='base' padding='base'>
|
|
74
|
+
<Heading level={2}>Account</Heading>
|
|
75
|
+
</Panel>
|
|
76
|
+
</Panels>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Bad
|
|
80
|
+
|
|
81
|
+
Each block is wrong. The comment says why.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// Row is not a direct child of Rows
|
|
85
|
+
<Panel>
|
|
86
|
+
<Row>
|
|
87
|
+
<Text>Hello</Text>
|
|
88
|
+
</Row>
|
|
89
|
+
</Panel>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Rows of one Row: put the content in the Panel or a Div
|
|
94
|
+
<Rows>
|
|
95
|
+
<Row>
|
|
96
|
+
<Text>Hello</Text>
|
|
97
|
+
</Row>
|
|
98
|
+
</Rows>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// Col is not a Rows child
|
|
103
|
+
<Rows>
|
|
104
|
+
<Col>
|
|
105
|
+
<Text>Hello</Text>
|
|
106
|
+
</Col>
|
|
107
|
+
<Row>
|
|
108
|
+
<Text>World</Text>
|
|
109
|
+
</Row>
|
|
110
|
+
</Rows>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// Copy is not in a type primitive, so it misses --panelize-text
|
|
115
|
+
<Panel>Hello</Panel>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// A control positions itself. Button has no alignX
|
|
120
|
+
<Col>
|
|
121
|
+
<Button alignX='center' label='Save' />
|
|
122
|
+
</Col>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Center with `alignX` on the wrapper (`Cols`, `Row`, `Panel`), not on `Button`.
|
|
126
|
+
|
|
127
|
+
## Which primitive?
|
|
128
|
+
|
|
129
|
+
| You want | Use |
|
|
130
|
+
| --------------------------- | -------------------------------------------------- |
|
|
131
|
+
| The outer frame of a view | `Panels` |
|
|
132
|
+
| A surface (nav, main, card) | `Panel` |
|
|
133
|
+
| A vertical list of bands | `Rows` of two or more `Row` |
|
|
134
|
+
| A horizontal list of items | `Cols` of two or more `Col` |
|
|
135
|
+
| A two-dimensional layout | `Grid` of two or more `Cell` |
|
|
136
|
+
| One wrapper or one band | `Div`, or the `Panel` itself |
|
|
137
|
+
| Words on screen | `Text` / `Heading` / `Span` / `TextLink` / `Label` |
|
|
138
|
+
| Copyable starting points | [recipes](recipes.md) |
|
package/docs/controls.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Controls
|
|
2
|
+
|
|
3
|
+
Behavior notes for each control: chrome, defaults, edge cases. Props are in `node_modules/panelize/dist/index.d.ts`. Examples live in the [README](../README.md#controls). Value ownership and validation: [forms](forms.md). Labelling and keyboard: [accessibility](accessibility.md).
|
|
4
|
+
|
|
5
|
+
## Button
|
|
6
|
+
|
|
7
|
+
`<button type="button">`. Caption: `label` (preferred). Custom insides: `children` (`Cols` / `Text` / host icons) — they win if both are set. A string as `children` still works; don’t prefer it. No visible text: `aria-label`. Omit chrome = filled chroma; `outline` = border + slot label, hollow at rest/hover/active. `color` is the chroma slot (`primary` / `secondary` / `accent` / `danger` / `warning` / `success` / `info`) for rest, hover, and active (outline border stays on this slot). Filled ink is `defaults.button.ink` (library `palette.black`), not chroma-on-fill. `size` is font size; `fill` is the box (100% of parent). Native `disabled` dims and skips hover. Instance `padding` / `paddingX` / `paddingY` / `borderWidth` / `borderRadius` override the theme. `background` / `ink` are a token/CSS (all steps) or `{ base, hover, active }`; omitted keys keep the slot / black ink / transparent outline. `background` on `outline` opts in to fill. No margin.
|
|
8
|
+
|
|
9
|
+
## Chip
|
|
10
|
+
|
|
11
|
+
`<span>` badge. Label is the chroma slot (`primary` / `secondary` / `accent` / `danger` / …). Fill is `color-mix` of that chroma at `defaults.chip.tint` (`12%`) into the nearest panel `--panelize-bg` until `background`. Capsule radius until `borderRadius`. `borderWidth` default `none`. Caption: `label` (preferred); `children` win. Instance `ink` / `borderColor` / `background` are surface / brand / status / CSS. `base` on those is the panel `base` surface (fill vs text vs border by prop), not a brand slot. Not a layout shell.
|
|
12
|
+
|
|
13
|
+
## Input
|
|
14
|
+
|
|
15
|
+
`<input type="text">` unless `type` is set. Rest chrome is the nearest `Panel` (bg, text, border, muted placeholder). **`color` is focus chroma only** (caret, border, glow) — not the typed text (`ink`). Instance `background` / `ink` / `borderColor` override rest fill, glyphs, and rest stroke (surface / brand / status / CSS). `base` means that surface’s fill, `text.base`, or `border` depending on the prop. Instance `borderWidth` / `borderRadius` override the theme. Focus / `:user-invalid` still own the stroke. After the user has edited, `:user-invalid` paints `status.danger` (same 2px / 18% glow, caret included). Hosts can also set `aria-invalid="true"` for submit-time paint without a prior edit. Pair a visible name with `Label htmlFor`; placeholder-only still needs `aria-label`. Native HTML `size` is omitted; `size` is font size. `fill` is the box. `ref` works as a normal prop. Host owns the value. `readOnly` dims like `disabled` but keeps a text cursor.
|
|
16
|
+
|
|
17
|
+
## NumberInput
|
|
18
|
+
|
|
19
|
+
`Input` locked to `type="number"`. Same chrome (`css`, `background`, `ink`, `borderColor`, `:user-invalid` / `aria-invalid`). Native `min` / `max` / `step` (`step='any'` allowed). No `type` prop. UA stepper arrows are hidden (`appearance: none`); bounds and step are the API. `onChange` still gives a string — use `event.currentTarget.valueAsNumber`. Wheel over the field blurs so the value does not accidentally step. Prefer `NumberInput` over `<Input type='number'>`.
|
|
20
|
+
|
|
21
|
+
## Textarea
|
|
22
|
+
|
|
23
|
+
Same field chrome as `Input` (`<textarea>`), including `:user-invalid` / `aria-invalid`. Theme field keys inherit `defaults.input` unless `defaults.textarea` overrides them. Default `rows={3}`. Resize is off until `resize`. `fill` stretches the box and does not lock resize.
|
|
24
|
+
|
|
25
|
+
## Select
|
|
26
|
+
|
|
27
|
+
Custom listbox (not a native `<select>`). Trigger uses the same field chrome as `Input` (`background` / `ink` / `borderColor` / `aria-invalid`). Theme field keys inherit `defaults.input` unless `defaults.select` overrides them (`listMaxHeight` / `listOffset` / option padding / `listZIndex` / `listShadow`). The menu portals to `document.body` (`position: fixed`) so a scrolling `Panels` does not clip it. Host owns `value` / `onChange` (`(value: string) => void`; empty string when cleared). A value that is not in `options` still shows (raw `value`) and can be cleared. Omit `open` for internal open state, or pass `open` + `onOpenChange`. Options: `options={{ value, label, disabled? }[]}` and/or child `<Option value>` (children win on the same `value`). `placeholder` when empty (muted). `required` blocks clearing to empty and marks the hidden input `required`. `clearable` shows an × when there is a value (ignored when `required`). `selectedAtTop` lists the selected option first in the menu (the `emptyOption` row stays first). `emptyOption` adds a first row with value `''` (`true` uses `placeholder`; a string is the caption; ignored when `required`). `readOnly` opens to inspect without pick / clear (not dimmed). Without `required`, clicking the selected option deselects it. `name` writes a hidden input for form submit (`disabled` when the select is). Pointer: pick, clear, caret-toggle close, and click-outside **blur** the trigger (no open ring). Keyboard pick / Escape keep focus. Tab away closes the menu. Keyboard: arrows, Enter, Space, Escape on the trigger (not `window`), Home, End, type-ahead; Backspace / Delete clears when clearable. `aria-autocomplete="none"`. The listbox uses `aria-labelledby` only when `id` is set. Disabled options are skipped. CSS chevron on the trigger; the value and rest option labels share highlighted ink. The selected option uses `color` (primary) for the label plus a CSS check, with no row fill. Hover (and keyboard active) fills the row only — text color does not change. No native fallback, multi-select, combobox typing, or optgroups. Prefer `Label` + `htmlFor`. `fill` stretches the trigger. Not a layout shell.
|
|
28
|
+
|
|
29
|
+
## MultiSelect
|
|
30
|
+
|
|
31
|
+
Custom multi listbox (not `multiple` on `Select`). Same field chrome, portal menu, and `defaults.select` as `Select`. Host owns `value` / `onChange` (`string[]`; empty array when cleared). The trigger shows a count (`2 selected`) — single line, no chips, English caption. The menu stays open while toggling. Each row is a `Checkbox` plus label; checked rows use `color` (primary) for the label, matching `Select`. Hover fills the row only. `clearable` is clear-all. `required` blocks an empty set (cannot uncheck the last). `selectedAtTop` lists checked options first (host order within groups); the highlight follows the same value after a toggle. `readOnly` opens to inspect without toggle / clear (not dimmed; row checkboxes stay enabled visually). No `emptyOption`. `name` writes one hidden input per value (`value=''` when empty, `required` on that input). Pointer: clear, caret-toggle close, and click-outside **blur** the trigger (toggles do not). Keyboard toggle / Escape keep focus. Tab away closes the menu. Keyboard: arrows, Enter / Space toggle, Escape on the trigger (not `window`), Home, End, type-ahead; Backspace / Delete clears when clearable. Type-ahead while **closed** opens and highlights (does not toggle). Disabled options are skipped. CSS chevron on the trigger. Prefer `Label` + `htmlFor`. `fill` stretches the trigger. Not a layout shell.
|
|
32
|
+
|
|
33
|
+
## Switch
|
|
34
|
+
|
|
35
|
+
`<input type="checkbox" role="switch">`. Native `checked` / `defaultChecked` / `onChange` — host owns the value. **`color` is the on-track chroma** (`primary` / `secondary` / `accent` / status; theme default `primary`). Off-track mixes panel text into `--panelize-bg` until `background`. Thumb is `defaults.switch.ink` (`palette.white`) until `ink`. Pair a visible name with `Label htmlFor`. No `label` / `children`. No `fill`. `size` is the type token (`extraSmall` … `extraLarge`). Track height is that size × theme `lineHeight` (keep `lineHeight` unitless, default `1.2`). Width is type × `defaults.switch.widthFactor` (default `2`). Thumb is height − 2×inset (`insetFactor` × type, default `0.2`). Pair with a `Label` of the **same** `size`. On-track hover/active use the `color` slot (same steps as `Button`). Instance `width` / `height` are space tokens or CSS and override those axes. Instance `borderRadius` (theme default `999px` capsule) shapes the track; the thumb uses `min(that, 50%)` so a capsule stays round and `small` / `none` squares both. `onChange` is the action (`event.currentTarget.checked`). Native `disabled` dims like `Button`. `:focus-visible` uses the on-track chroma glow (same mix as fields). Not a layout shell.
|
|
36
|
+
|
|
37
|
+
## Checkbox
|
|
38
|
+
|
|
39
|
+
`<input type="checkbox">`. Native `checked` / `defaultChecked` / `onChange` — host owns the value. **`color` is the chroma slot** (`primary` / `secondary` / `accent` / status; theme default `primary`), same steps as `Button`. Empty rest / hover / active stroke is `currentColor` (wrapping `Label` ink, else panel `text.base`) until `borderColor`. Checked / indeterminate fill the `color` slot’s `.base` / `.hover` / `.active`. Rest fill is transparent until `background`. Check / dash is `defaults.checkbox.ink` (`palette.black`, same as filled `Button`) until `ink` (token/CSS or `{ base, hover, active }`; hover/active unused on the mark). Pair a visible name with `Label htmlFor`. No `label` / `children`. No `fill`. `size` is the type token; the box is that size (`1em`, square). Pair with a `Label` of the **same** `size`. Instance `width` / `height` are space tokens or CSS. Instance `borderRadius` (theme default `small`) and `borderWidth` (theme default `extraSmall`, `1px`) override the theme. `indeterminate` is the native property (dash); it does not set `checked`. `hideCheckIcon` is fill-only when checked (no check glyph); the dash still draws when indeterminate. Empty `disabled` keeps the inherited stroke (pair with a muted wrapping `Label`); checked / indeterminate still dim like `Button`. Disabled skips hover. `:focus-visible` uses the on-fill chroma glow. Not a layout shell.
|
|
40
|
+
|
|
41
|
+
## Radio
|
|
42
|
+
|
|
43
|
+
`<input type="radio">`. Native `name` / `value` / `checked` / `defaultChecked` / `onChange` — host owns the group. **`color` is the chroma slot** (`primary` / `secondary` / `accent` / status; theme default `primary`), same steps as `Button`. Empty rest / hover / active stroke is `currentColor` (wrapping `Label` ink, else panel `text.base`) until `borderColor`. Checked ring and inner disc use the `color` slot’s `.base` / `.hover` / `.active`. Rest fill stays transparent until `background` so the panel shows in the gap around the disc. `ink` is the disc (`PaintSteps`, same as `Checkbox`); omit = disc follows `color` including hover/active. Pair a visible name with a wrapping `Label htmlFor`. No `label` / `children`. No `fill`. `size` is the type token; the box is that size (`1em`, circle). Pair with a `Label` of the **same** `size`. Disc inset is type × `defaults.radio.insetFactor` (default `0.22`). Instance `width` / `height` are space tokens or CSS. Instance `borderRadius` (theme default `999px` circle) shapes the ring; the disc uses `min(that, 50%)`. Instance `borderWidth` (theme default `extraSmall`, `1px`) overrides the theme. Empty `disabled` keeps the inherited stroke (pair with a muted wrapping `Label`); checked still dims like `Button`. Disabled skips hover. `:focus-visible` uses the on-ring chroma glow. Not a layout shell.
|
|
44
|
+
|
|
45
|
+
## Label
|
|
46
|
+
|
|
47
|
+
`<label>`. Ink like `Text` (`color` / `muted` / `highlighted`). The line box hugs the glyphs (`line-height: 1` and `text-box: trim-both cap alphabetic`, same idea as `Heading`) so a `Switch` / `Checkbox` / `Radio` in a centered `Cols` row lines up. Fields use `ink` for glyphs; field `color` is focus chroma only. Caption: `label` (preferred). Custom insides: `children` — they win if both are set. Point `htmlFor` at the control’s `id`. Cursor is `pointer` when `htmlFor` / `onClick` is set or the label wraps a control (`button` / `input` / `select` / `textarea`). Wrap the control (not a sibling-only `htmlFor`) so a `disabled` target is `not-allowed` via `:has(:disabled)`. Field labels stay `pointer`; the field itself keeps `text` / `not-allowed`.
|
package/docs/forms.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Forms, buttons, chips (bonus)
|
|
2
|
+
|
|
3
|
+
Core is layout + text. These controls exist so a host can ship a complete kit. Still compose them **inside** `Panel` / `Row` / `Col` / `Cell` / `Div`. The wrapper places them.
|
|
4
|
+
|
|
5
|
+
Per-control behavior notes: [controls](controls.md). Full prop list: exported types in `node_modules/panelize/dist/index.d.ts` (JSDoc on every prop). Human detail: [README.md](../README.md). Copyable field layout: [recipes](recipes.md#form-fields). Keyboard and labelling rules: [accessibility](accessibility.md).
|
|
6
|
+
|
|
7
|
+
- `Button` — `label` preferred; `children` win. `color` is the chroma slot. `outline` stays hollow. `fill` stretches the button, not a panel.
|
|
8
|
+
- `Chip` — badge, not a button. No hover.
|
|
9
|
+
- `Input` / `NumberInput` / `Textarea` — field chrome. Instance `color` is **focus** chroma, not glyph ink (`ink`). Prefer `Label` + `htmlFor`.
|
|
10
|
+
- `Select` / `MultiSelect` — custom listboxes, not native `<select>`. Host `value` / `onChange`. `MultiSelect` is a count in the field (`N selected`), not chips.
|
|
11
|
+
- `Switch` / `Checkbox` / `Radio` — wrap with `Label` (same `size`). Empty checkbox/radio stroke follows the label (`currentColor`).
|
|
12
|
+
- `Label` — type-like ink; not a layout shell.
|
|
13
|
+
|
|
14
|
+
Do not add `alignX` / `flex` / padding-as-layout on these to position them. Use a `Col` / `Row` / `Div`.
|
|
15
|
+
|
|
16
|
+
## Value ownership
|
|
17
|
+
|
|
18
|
+
The host owns every value. Nothing here keeps state you did not ask for.
|
|
19
|
+
|
|
20
|
+
| Control | Controlled | Uncontrolled |
|
|
21
|
+
| --------------------- | ----------------------------------------------------------------------------------------------- | ---------------- |
|
|
22
|
+
| `Input` etc. | `value` + `onChange(event)` | `defaultValue` |
|
|
23
|
+
| `NumberInput` | `value` + `onChange(event)`; read `event.currentTarget.valueAsNumber` (the `value` is a string) | `defaultValue` |
|
|
24
|
+
| `Select` | `value: string` + `onChange(value: string)` | not offered |
|
|
25
|
+
| `MultiSelect` | `value: string[]` + `onChange(value: string[])` | not offered |
|
|
26
|
+
| `Switch` / `Checkbox` | `checked` + `onChange(event)` | `defaultChecked` |
|
|
27
|
+
| `Radio` | `name` + `value` + `checked` + `onChange(event)` | `defaultChecked` |
|
|
28
|
+
|
|
29
|
+
`Select` / `MultiSelect` call `onChange` with plain values, not events. Cleared is `''` (`Select`) or `[]` (`MultiSelect`). A `value` that has no matching option is still shown.
|
|
30
|
+
|
|
31
|
+
## `Select` and `MultiSelect`
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<Label htmlFor='country' label='Country' />
|
|
35
|
+
<Select id='country' value={country} onChange={setCountry} placeholder='Choose' clearable>
|
|
36
|
+
<Option value='us'>United States</Option>
|
|
37
|
+
<Option value='mx' disabled>Mexico</Option>
|
|
38
|
+
</Select>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Options: `<Option value>` children and/or `options={[{ value, label, disabled? }]}`. Children win on a duplicate `value`.
|
|
42
|
+
- The menu renders in a portal on `document.body` (`position: fixed`). A scrolling `Panels` does not clip it. It sits at `defaults.select.listZIndex` (default 1100); raise that in the theme if a host overlay covers it.
|
|
43
|
+
- `clearable` shows ×. `required` blocks an empty value and disables clearing. `selectedAtTop` lists the selected option(s) first. `Select` also has `emptyOption`. `readOnly` opens to inspect without changing.
|
|
44
|
+
- `name` writes hidden input(s) so a native `<form>` submits the value. `MultiSelect` writes one input per value.
|
|
45
|
+
- `open` / `onOpenChange` control the menu. Omit for internal state.
|
|
46
|
+
- `MultiSelect` stays open while toggling and shows `N selected`. The caption is English-only.
|
|
47
|
+
- No native fallback, optgroups, or typing into the field.
|
|
48
|
+
|
|
49
|
+
## `Switch`, `Checkbox`, `Radio`
|
|
50
|
+
|
|
51
|
+
Wrap the control in a `Label` and give both the same `size`. A sibling caption works only with `htmlFor`, and a `disabled` control then cannot dim its label.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<Label size='small'>
|
|
55
|
+
<Checkbox size='small' checked={agree} onChange={(e) => setAgree(e.currentTarget.checked)} /> I
|
|
56
|
+
agree
|
|
57
|
+
</Label>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`Checkbox` supports `indeterminate` (the native property; it does not set `checked`). A `Radio` group shares `name`; the host decides which `value` is checked.
|
|
61
|
+
|
|
62
|
+
## Validation
|
|
63
|
+
|
|
64
|
+
`Input` / `NumberInput` / `Textarea` / `Select` paint `status.danger` for `:user-invalid` (after the user edits) and for `aria-invalid='true'`. Set `aria-invalid` yourself for submit-time errors. Show the message in a `Text color='danger'` and tie it to the field with `aria-describedby`.
|
package/docs/layout.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Layout vocabulary
|
|
2
|
+
|
|
3
|
+
Space / size / border tokens: `none` | `extraSmall` | `small` | `base` | `large` | `extraLarge`. Breakpoints: `sm` | `md` | `lg`. Token or CSS length — not a unitless number. Boxes are `border-box`.
|
|
4
|
+
|
|
5
|
+
| Prop | Meaning |
|
|
6
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
7
|
+
| `fill` | Size to parent. Absolute `Panels`: `inset: 0`. Else height / width 100%. `Row` theme height `auto` → width 100% only. `Col` theme width `auto` → height 100% only. Explicit `width` / `height` win. Not on `Cell`. |
|
|
8
|
+
| `width` / `height` | All layout except `Cell`. `Div` omit = unset. |
|
|
9
|
+
| `flex` | `flex: 1 1 0%` plus `min-width` / `min-height` 0 (nested `scroll` can shrink). Not `fill`. `Panel` / `Row` / `Col` / `Div` are `flex-shrink: 0` until `flex`. |
|
|
10
|
+
| `alignX` / `alignY` | Screen axes. Set on the **wrapper**. Track omit: flex cross `stretch` / main `start`; `Grid` `stretch` / `stretch`. Child omit: inherit. |
|
|
11
|
+
| `gap` | `Panels`, `Rows`, `Cols`, `Grid`. |
|
|
12
|
+
| `wrap` | `Rows` / `Cols`. Not `stack`. Not `Grid`. |
|
|
13
|
+
| `stack` | `horizontal` `Panels` / `Cols` / `Grid`. Container query, **at most** this wide → column. |
|
|
14
|
+
| `stackCols` | `Grid` + `stack`: column count while stacked. Default 1. |
|
|
15
|
+
| `numCols` / `numRows` | `Grid` only. Equal `minmax(0, 1fr)` tracks. |
|
|
16
|
+
| `spanX` / `spanY` | `Cell` only. Default 1. `stack` forces `spanX` to 1 when `stackCols` is 1. |
|
|
17
|
+
| `scroll` | `Panels` / `Panel` only. See [shell](shell.md). |
|
|
18
|
+
| `relative` / `horizontal` | `Panels` only. |
|
|
19
|
+
|
|
20
|
+
## `Div`
|
|
21
|
+
|
|
22
|
+
Catch-all box (spacing, size, border, `fill` / `flex`). No background or color of its own. Use it when you need one wrapper and a `Rows` of one `Row` would be wrong. Direct children: anything except a stray `Row` / `Col` / `Cell` (those still belong under their tracks).
|
|
23
|
+
|
|
24
|
+
## Tracks
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<Rows gap='small' alignX='center'>
|
|
28
|
+
<Row>
|
|
29
|
+
<Text>One</Text>
|
|
30
|
+
</Row>
|
|
31
|
+
<Row>
|
|
32
|
+
<Text>Two</Text>
|
|
33
|
+
</Row>
|
|
34
|
+
</Rows>
|
|
35
|
+
|
|
36
|
+
<Cols stack='sm' gap='small'>
|
|
37
|
+
<Col width='12rem'>
|
|
38
|
+
<Text>Side</Text>
|
|
39
|
+
</Col>
|
|
40
|
+
<Col flex>
|
|
41
|
+
<Text>Main</Text>
|
|
42
|
+
</Col>
|
|
43
|
+
</Cols>
|
|
44
|
+
|
|
45
|
+
<Grid numCols={3} stackCols={2} gap='small' stack='sm'>
|
|
46
|
+
<Cell>
|
|
47
|
+
<Text>A</Text>
|
|
48
|
+
</Cell>
|
|
49
|
+
<Cell spanX={2}>
|
|
50
|
+
<Text>B</Text>
|
|
51
|
+
</Cell>
|
|
52
|
+
</Grid>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`Cell` has no `width` / `height` / `fill`. Size comes from the grid tracks and `spanX` / `spanY`. `Cell` is `min-width` / `min-height` 0 so content can shrink.
|
|
56
|
+
|
|
57
|
+
## Borders
|
|
58
|
+
|
|
59
|
+
Every layout box. Width cascade like padding: `borderWidth` → `borderX` / `Y` → side. `borderColor` is surface, brand, or status — not a `Panel` `variant`. Default width `none`.
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Copyable trees that follow [composition](composition.md). Pick the closest recipe, then change copy and tokens. Every tree sits inside `PanelizeProvider` and a host with `html` / `body` / `#root { height: 100% }`.
|
|
4
|
+
|
|
5
|
+
| Need | Recipe |
|
|
6
|
+
| ------------------------------- | ------------------------------------------- |
|
|
7
|
+
| One scrolling page | [Page](#page) |
|
|
8
|
+
| Sidebar plus main | [Sidebar and main](#sidebar-and-main) |
|
|
9
|
+
| Centered card (login, empty) | [Centered card](#centered-card) |
|
|
10
|
+
| Label above a field | [Form fields](#form-fields) |
|
|
11
|
+
| Metric tiles that reflow | [Dashboard grid](#dashboard-grid) |
|
|
12
|
+
| Header bar, content, action row | [Header, body, footer](#header-body-footer) |
|
|
13
|
+
|
|
14
|
+
## Page
|
|
15
|
+
|
|
16
|
+
Scroll the shell. Inner `Panel` uses `minHeight='100%'`, not `fill`.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<Panels fill scroll='vertical'>
|
|
20
|
+
<Panel variant='muted' minHeight='100%' padding='base'>
|
|
21
|
+
<Rows gap='base'>
|
|
22
|
+
<Row>
|
|
23
|
+
<Heading level={1}>Title</Heading>
|
|
24
|
+
</Row>
|
|
25
|
+
<Row>
|
|
26
|
+
<Text muted>Long page copy…</Text>
|
|
27
|
+
</Row>
|
|
28
|
+
</Rows>
|
|
29
|
+
</Panel>
|
|
30
|
+
</Panels>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Sidebar and main
|
|
34
|
+
|
|
35
|
+
`horizontal` + `fill` on `Panels`. Sidebar gets `width`. Main gets `flex` and owns `scroll`. `stack` is "at most this wide → column".
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Panels horizontal fill stack='sm'>
|
|
39
|
+
<Panel variant='muted' width='14rem' padding='base'>
|
|
40
|
+
<Rows gap='small'>
|
|
41
|
+
<Row>
|
|
42
|
+
<TextLink href='/'>Home</TextLink>
|
|
43
|
+
</Row>
|
|
44
|
+
<Row>
|
|
45
|
+
<TextLink href='/settings'>Settings</TextLink>
|
|
46
|
+
</Row>
|
|
47
|
+
</Rows>
|
|
48
|
+
</Panel>
|
|
49
|
+
<Panel variant='base' flex scroll='vertical' padding='base'>
|
|
50
|
+
<Heading level={2}>Main</Heading>
|
|
51
|
+
</Panel>
|
|
52
|
+
</Panels>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Centered card
|
|
56
|
+
|
|
57
|
+
Center on `Panels` (not scrolling). One `Panel` is the card.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<Panels fill alignX='center' alignY='center'>
|
|
61
|
+
<Panel variant='muted' width='24rem' padding='large' borderWidth='extraSmall' borderRadius='base'>
|
|
62
|
+
<Rows gap='base'>
|
|
63
|
+
<Row>
|
|
64
|
+
<Heading level={2}>Sign in</Heading>
|
|
65
|
+
</Row>
|
|
66
|
+
<Row>
|
|
67
|
+
<Button fill label='Continue' />
|
|
68
|
+
</Row>
|
|
69
|
+
</Rows>
|
|
70
|
+
</Panel>
|
|
71
|
+
</Panels>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Form fields
|
|
75
|
+
|
|
76
|
+
Each field is a `Rows` of two `Row`s (label, control). The outer `Rows` spaces the fields. Controls take `fill` to stretch themselves; the `Row` places them.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
<Rows gap='base'>
|
|
80
|
+
<Row>
|
|
81
|
+
<Rows gap='extraSmall'>
|
|
82
|
+
<Row>
|
|
83
|
+
<Label htmlFor='email' label='Email' />
|
|
84
|
+
</Row>
|
|
85
|
+
<Row>
|
|
86
|
+
<Input id='email' type='email' fill />
|
|
87
|
+
</Row>
|
|
88
|
+
</Rows>
|
|
89
|
+
</Row>
|
|
90
|
+
<Row>
|
|
91
|
+
<Rows gap='extraSmall'>
|
|
92
|
+
<Row>
|
|
93
|
+
<Label htmlFor='plan' label='Plan' />
|
|
94
|
+
</Row>
|
|
95
|
+
<Row>
|
|
96
|
+
<Select id='plan' value={plan} onChange={setPlan} fill>
|
|
97
|
+
<Option value='hobby'>Hobby</Option>
|
|
98
|
+
<Option value='pro'>Pro</Option>
|
|
99
|
+
</Select>
|
|
100
|
+
</Row>
|
|
101
|
+
</Rows>
|
|
102
|
+
</Row>
|
|
103
|
+
</Rows>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A toggle pairs with a **wrapping** `Label` of the same `size`:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<Label htmlFor='auto-save'>
|
|
110
|
+
<Switch id='auto-save' defaultChecked /> Auto-save drafts
|
|
111
|
+
</Label>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Dashboard grid
|
|
115
|
+
|
|
116
|
+
`stack='sm'` collapses to `stackCols` columns at most `sm` wide.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<Grid numCols={3} stackCols={1} stack='md' gap='base'>
|
|
120
|
+
<Cell>
|
|
121
|
+
<Panel variant='muted' padding='base'>
|
|
122
|
+
<Text muted size='small'>
|
|
123
|
+
Users
|
|
124
|
+
</Text>
|
|
125
|
+
<Heading level={3}>1,204</Heading>
|
|
126
|
+
</Panel>
|
|
127
|
+
</Cell>
|
|
128
|
+
<Cell>
|
|
129
|
+
<Panel variant='muted' padding='base'>
|
|
130
|
+
<Text muted size='small'>
|
|
131
|
+
Revenue
|
|
132
|
+
</Text>
|
|
133
|
+
<Heading level={3}>$8.4k</Heading>
|
|
134
|
+
</Panel>
|
|
135
|
+
</Cell>
|
|
136
|
+
<Cell>
|
|
137
|
+
<Panel variant='muted' padding='base'>
|
|
138
|
+
<Text muted size='small'>
|
|
139
|
+
Errors
|
|
140
|
+
</Text>
|
|
141
|
+
<Heading level={3}>3</Heading>
|
|
142
|
+
</Panel>
|
|
143
|
+
</Cell>
|
|
144
|
+
</Grid>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Header, body, footer
|
|
148
|
+
|
|
149
|
+
A default `Panels` is a column. The body takes `flex` and scrolls; the bars keep their size.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<Panels fill>
|
|
153
|
+
<Panel variant='highlighted' padding='base' borderBottom='extraSmall'>
|
|
154
|
+
<Heading level={2}>Project</Heading>
|
|
155
|
+
</Panel>
|
|
156
|
+
<Panel flex scroll='vertical' padding='base'>
|
|
157
|
+
<Text>Body…</Text>
|
|
158
|
+
</Panel>
|
|
159
|
+
<Panel variant='muted' padding='base' borderTop='extraSmall'>
|
|
160
|
+
<Cols gap='small' alignX='end'>
|
|
161
|
+
<Col>
|
|
162
|
+
<Button outline label='Cancel' />
|
|
163
|
+
</Col>
|
|
164
|
+
<Col>
|
|
165
|
+
<Button label='Save' />
|
|
166
|
+
</Col>
|
|
167
|
+
</Cols>
|
|
168
|
+
</Panel>
|
|
169
|
+
</Panels>
|
|
170
|
+
```
|