division-ds 0.1.1 → 0.2.0

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.
Files changed (97) hide show
  1. package/AGENTS.md +16 -2
  2. package/README.md +11 -0
  3. package/division-ds-0.2.0.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +205 -0
  6. package/public/docs/components/ai-label.md +88 -0
  7. package/public/docs/components/alert-dialog.md +290 -0
  8. package/public/docs/components/alert.md +173 -0
  9. package/public/docs/components/approval-request.md +146 -0
  10. package/public/docs/components/auth-form.md +130 -0
  11. package/public/docs/components/avatar-group.md +150 -0
  12. package/public/docs/components/avatar.md +177 -0
  13. package/public/docs/components/badge.md +176 -0
  14. package/public/docs/components/breadcrumbs.md +110 -0
  15. package/public/docs/components/button.md +202 -0
  16. package/public/docs/components/card.md +255 -0
  17. package/public/docs/components/carousel.md +128 -0
  18. package/public/docs/components/cart.md +170 -0
  19. package/public/docs/components/chart-panel.md +356 -0
  20. package/public/docs/components/chat-message.md +154 -0
  21. package/public/docs/components/chat-thread.md +242 -0
  22. package/public/docs/components/checkbox.md +147 -0
  23. package/public/docs/components/checkout-form.md +126 -0
  24. package/public/docs/components/command-palette.md +145 -0
  25. package/public/docs/components/data-table.md +399 -0
  26. package/public/docs/components/description-list.md +101 -0
  27. package/public/docs/components/dialog.md +195 -0
  28. package/public/docs/components/divider.md +95 -0
  29. package/public/docs/components/dropdown-menu.md +220 -0
  30. package/public/docs/components/empty-state.md +143 -0
  31. package/public/docs/components/footer.md +395 -0
  32. package/public/docs/components/form-field.md +213 -0
  33. package/public/docs/components/icon-button.md +125 -0
  34. package/public/docs/components/input.md +198 -0
  35. package/public/docs/components/kanban-board.md +298 -0
  36. package/public/docs/components/kbd.md +70 -0
  37. package/public/docs/components/link.md +96 -0
  38. package/public/docs/components/list-item.md +238 -0
  39. package/public/docs/components/loading-animation.md +70 -0
  40. package/public/docs/components/multi-select.md +200 -0
  41. package/public/docs/components/navbar.md +142 -0
  42. package/public/docs/components/newsletter-form.md +110 -0
  43. package/public/docs/components/page-header.md +149 -0
  44. package/public/docs/components/pagination.md +210 -0
  45. package/public/docs/components/product-card.md +169 -0
  46. package/public/docs/components/progress.md +99 -0
  47. package/public/docs/components/prompt-input.md +149 -0
  48. package/public/docs/components/quantity-stepper.md +138 -0
  49. package/public/docs/components/radio.md +140 -0
  50. package/public/docs/components/rating.md +104 -0
  51. package/public/docs/components/score-ring.md +93 -0
  52. package/public/docs/components/search.md +114 -0
  53. package/public/docs/components/section-heading.md +111 -0
  54. package/public/docs/components/segmented-control.md +124 -0
  55. package/public/docs/components/select.md +222 -0
  56. package/public/docs/components/sheet.md +143 -0
  57. package/public/docs/components/sidebar.md +191 -0
  58. package/public/docs/components/skeleton.md +133 -0
  59. package/public/docs/components/slider.md +112 -0
  60. package/public/docs/components/sparkline.md +126 -0
  61. package/public/docs/components/spinner.md +96 -0
  62. package/public/docs/components/stat.md +104 -0
  63. package/public/docs/components/status-tracker.md +143 -0
  64. package/public/docs/components/stepper.md +145 -0
  65. package/public/docs/components/switch.md +151 -0
  66. package/public/docs/components/tabs.md +178 -0
  67. package/public/docs/components/tag.md +103 -0
  68. package/public/docs/components/textarea.md +146 -0
  69. package/public/docs/components/timeline.md +150 -0
  70. package/public/docs/components/toast.md +192 -0
  71. package/public/docs/components/tool-call.md +105 -0
  72. package/public/docs/components/tooltip.md +106 -0
  73. package/public/manifest/components.json +189 -7
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +76 -37
  76. package/scripts/lib/agent-docs.mjs +59 -2
  77. package/scripts/lib/ai-copy.mjs +106 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +44 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +115 -0
  83. package/scripts/lib/mcp-tools.mjs +241 -0
  84. package/scripts/lib/props-validator.mjs +1 -1
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/skills/division-ds/SKILL.md +90 -0
  87. package/skills/division-ds/commands/add.md +25 -0
  88. package/skills/division-ds/commands/build.md +46 -0
  89. package/skills/division-ds/commands/fix.md +28 -0
  90. package/skills/division-ds/commands/review.md +25 -0
  91. package/skills/division-ds/topics/ai.md +7 -0
  92. package/skills/division-ds/topics/colour.md +11 -0
  93. package/skills/division-ds/topics/feedback.md +9 -0
  94. package/skills/division-ds/topics/forms.md +19 -0
  95. package/skills/division-ds/topics/layout.md +34 -0
  96. package/skills/division-ds/topics/motion.md +10 -0
  97. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,124 @@
1
+ # SegmentedControl
2
+
3
+ > Two to five options in one framed row, with one selected. It changes what's on screen in place. Under the hood it's a radio group: Tab stops on it once, the arrow keys move and select, and the selected option has a signal fill with ink text.
4
+
5
+ - **Best for:** Switching the view of the same content in place: Board or Table, 24 h or 7 d.
6
+ - **Not for:** Panels of different content: use Tabs. A choice in a form: use Radio.
7
+ - Molecule. Source: `src/components/molecules/segmented-control/SegmentedControl.tsx`
8
+ - Rendered docs page: `/components/segmented-control`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { SegmentedControl } from 'src/components/molecules/segmented-control/SegmentedControl'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | Required. Names the group: "View", "Range". |
21
+ | `options` | `readonly SegmentedControlOption[]` | | Required. Two to five. |
22
+ | `value` | `string` | | Required. The selected option. |
23
+ | `onValueChange` | `(value: string) => void` | | Required. Called with the new value. |
24
+ | `size` | `'sm' \| 'md'` | `'md'` | The same height as Button's sm or md. |
25
+ | `className` | `string` | | Added to the group. |
26
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the group. |
27
+ | `value (SegmentedControlOption)` | `string` | | Required. What onValueChange receives. |
28
+ | `label (SegmentedControlOption)` | `string` | | Required. Always shown. |
29
+ | `icon (SegmentedControlOption)` | `ReactNode` | | A lucide icon before the label, aria-hidden so screen readers skip it. |
30
+
31
+ ## Types and helpers
32
+
33
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
34
+
35
+ ```ts
36
+ export type SegmentedControlOption = {
37
+ value: string
38
+ /** Always shown. The icon, if any, sits before it. */
39
+ label: string
40
+ /** A lucide icon, aria-hidden: the label names the option. */
41
+ icon?: ReactNode
42
+ }
43
+ ```
44
+
45
+ ## Examples
46
+
47
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
48
+
49
+ ### A time range
50
+
51
+ ```tsx
52
+ const RANGES = [
53
+ { value: '1h', label: '1 h' },
54
+ { value: '24h', label: '24 h' },
55
+ { value: '7d', label: '7 d' },
56
+ { value: '30d', label: '30 d' },
57
+ ]
58
+
59
+ function RangeExample() {
60
+ const [range, setRange] = useState('24h')
61
+ return (
62
+ <div className="flex flex-col items-center gap-3">
63
+ <SegmentedControl label="Range" options={RANGES} value={range} onValueChange={setRange} />
64
+ <p className="font-body text-sm text-[var(--color-text-secondary)]">Showing the last {RANGES.find(option => option.value === range)?.label}.</p>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ <RangeExample />
70
+ ```
71
+
72
+ ### Small, with icons, lined up with a small Button
73
+
74
+ ```tsx
75
+ function ToolbarExample() {
76
+ const [view, setView] = useState('board')
77
+ return (
78
+ <div className="flex flex-wrap items-center gap-3">
79
+ <SegmentedControl
80
+ label="View"
81
+ size="sm"
82
+ options={[
83
+ { value: 'board', label: 'Board', icon: <Columns3 /> },
84
+ { value: 'table', label: 'Table', icon: <Table /> },
85
+ ]}
86
+ value={view}
87
+ onValueChange={setView}
88
+ />
89
+ <Button size="sm" variant="secondary">
90
+ Export
91
+ </Button>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ <ToolbarExample />
97
+ ```
98
+
99
+ ## Keyboard
100
+
101
+ | Keys | Result |
102
+ |---|---|
103
+ | `Tab` | Moves into the group, onto the selected option, and out again. |
104
+ | `ArrowLeft` `ArrowRight` | Moves to the previous or next option and selects it. Wraps round at the ends. |
105
+ | `ArrowUp` `ArrowDown` | The same as Left and Right. |
106
+ | `Space` | Selects the focused option, if it isn't selected already. |
107
+
108
+ ## Do
109
+
110
+ - Use it where the change happens straight away and can be undone: the view, a time range, a unit.
111
+ - Keep labels to a word or two; they're set in uppercase monospace.
112
+ - Match the size to the buttons beside it: sm in a busy toolbar, md otherwise.
113
+
114
+ ## Don't
115
+
116
+ - Don't use it for panels of different content; that's Tabs, which links each tab to its panel.
117
+ - Don't use it for a choice that's sent with a form; that's a RadioGroup inside the form.
118
+ - Don't drop the label. Every option keeps its words, even with an icon.
119
+
120
+ ## Anatomy
121
+
122
+ - **Group**: A radiogroup named by label, framed with the control hairline. It scrolls sideways if it's too wide.
123
+ - **Option**: A radio with a monospace label and an optional icon, the same height as a Button at sm or md.
124
+ - **Selected option**: Signal fill, ink text, semibold. Its focus ring is drawn in ink.
@@ -0,0 +1,222 @@
1
+ # Select
2
+
3
+ > A single choice from a list, shown in a portalled listbox (a pop-up list rendered at the top level of the page, so nothing around it can clip it). Options come in as an array instead of as children, because the trigger, the panel and the look of each row are decisions the system makes once, not per use.
4
+
5
+ - **Best for:** One choice from a list of about five options or more.
6
+ - **Not for:** Several values: use MultiSelect. Fewer than about five options: use Radio.
7
+ - Atom. Source: `src/components/atoms/select/Select.tsx`
8
+ - Rendered docs page: `/components/select`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Select } from 'src/components/atoms/select/Select'
14
+ ```
15
+
16
+ ## Requires
17
+
18
+ - A <FormField> around it. On its own the control has no label. FormField supplies the label, the help text and the error, and connects their ids so assistive tech (screen readers and similar tools) reads them together.
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ |---|---|---|---|
24
+ | `label` | `string` | | Required. The trigger’s accessible name, the name a screen reader announces for it. |
25
+ | `options` | `SelectOptionWithIcon[]` | | { value, label, disabled?, icon? } rows shown in the listbox. Pass this or groups, never both. |
26
+ | `groups` | `SelectGroup[]` | | { label, options }[]: rows under headings. Pass this or options, never both. |
27
+ | `placeholder` | `string` | `'Select…'` | Shown until a value is chosen. |
28
+ | `value` | `string` | | The selection, when you control it from outside. Following Radix’s own convention, pass it together with onValueChange. |
29
+ | `defaultValue` | `string` | | The starting selection, when the select manages it itself (uncontrolled). Pass this or value, never both. |
30
+ | `onValueChange` | `(value: string) => void` | | Fires with the new value when a row is chosen, whether the select is controlled or uncontrolled. |
31
+ | `disabled` | `boolean` | | Disables the whole control. |
32
+ | `required` | `boolean` | | Marks the control as required for form submission. |
33
+ | `name` | `string` | | The form field name, for native form submission. |
34
+ | `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Matches Input’s sizes so the two line up. |
35
+ | `invalid` | `boolean` | `false` | Sets aria-invalid, which tells assistive tech the field has an error, and shows the invalid border. |
36
+ | `className` | `string` | | Merged with the trigger’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
37
+ | `ref` | `Ref<HTMLButtonElement>` | | Passed through to the trigger button. |
38
+ | `id` | `string` | | The trigger’s id. FormField sets it. |
39
+ | `aria-describedby` | `string` | | The ids of the help and error text, set on the trigger so a screen reader reads them with it. FormField sets it. |
40
+ | `triggerProps` | `Omit<ComponentPropsWithoutRef<'button'>, 'aria-label' \| 'aria-invalid' \| 'className'>` | | Extra props for the visible trigger button, e.g. data-testid. These are separate from the top-level rest, which goes to Radix’s Root (value, onValueChange, disabled, required, name, dir, form…) and never reaches an element on the page. aria-label and aria-invalid are typed away instead of being quietly overridden at runtime, because label and invalid already decide those. |
41
+ | `...rest` | `Omit<Select.Root props, 'children'>, minus every aria-* key and data-testid` | | Radix’s Root control props only. Every aria-* attribute and data-testid are typed away at the top level, because Root renders no element on the page, and anything that lands there vanishes with no error anywhere. Send them through triggerProps instead. children is typed away because this component renders its own Trigger and Portal. |
42
+
43
+ ## Types and helpers
44
+
45
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
46
+
47
+ ```ts
48
+ export type SelectOption = { value: string; label: string; disabled?: boolean }
49
+ ```
50
+
51
+ ```ts
52
+ /** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than
53
+ * an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders
54
+ * no icons: widening the shared type would let an icon compile there and vanish. */
55
+ export type SelectOptionWithIcon = SelectOption & {
56
+ /** Leading glyph, aria-hidden: the label names the row. Not repeated in the trigger. */
57
+ icon?: ReactNode
58
+ }
59
+ ```
60
+
61
+ ```ts
62
+ /** A titled run of rows. The heading is not selectable; it names the group for screen readers. */
63
+ export type SelectGroup = { label: string; options: SelectOptionWithIcon[] }
64
+ ```
65
+
66
+ ## Examples
67
+
68
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
69
+
70
+ ### Placeholder
71
+
72
+ ```tsx
73
+ const SPEED_OPTIONS = [
74
+ { value: 'standard', label: 'Standard' },
75
+ { value: 'priority', label: 'Priority' },
76
+ { value: 'overnight', label: 'Overnight', disabled: true },
77
+ ]
78
+
79
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} placeholder="Choose a speed" />
80
+ ```
81
+
82
+ ### With a value
83
+
84
+ ```tsx
85
+ const SPEED_OPTIONS = [
86
+ { value: 'standard', label: 'Standard' },
87
+ { value: 'priority', label: 'Priority' },
88
+ { value: 'overnight', label: 'Overnight', disabled: true },
89
+ ]
90
+
91
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} defaultValue="priority" />
92
+ ```
93
+
94
+ ### Groups with headings and icons
95
+
96
+ ```tsx
97
+ const DELIVERY_GROUPS: SelectGroup[] = [
98
+ {
99
+ label: 'Courier',
100
+ options: [
101
+ { value: 'standard', label: 'Standard', icon: <Truck /> },
102
+ { value: 'priority', label: 'Priority', icon: <Zap /> },
103
+ ],
104
+ },
105
+ {
106
+ label: 'Collect',
107
+ options: [
108
+ { value: 'depot', label: 'Brasov depot', icon: <Package /> },
109
+ { value: 'field', label: 'At the field', icon: <Crosshair /> },
110
+ ],
111
+ },
112
+ ]
113
+
114
+ <Select label="Delivery" groups={DELIVERY_GROUPS} placeholder="Choose delivery" />
115
+ ```
116
+
117
+ ### Sizes
118
+
119
+ ```tsx
120
+ const TERRAIN_OPTIONS = [
121
+ { value: 'woodland', label: 'Woodland' },
122
+ { value: 'urban', label: 'Urban' },
123
+ { value: 'desert', label: 'Desert' },
124
+ { value: 'indoor', label: 'Indoor' },
125
+ ]
126
+
127
+ <Select label="Terrain (small)" inputSize="sm" options={TERRAIN_OPTIONS} placeholder="Terrain" />
128
+ <Select label="Terrain (medium)" inputSize="md" options={TERRAIN_OPTIONS} placeholder="Terrain" />
129
+ <Select label="Terrain (large)" inputSize="lg" options={TERRAIN_OPTIONS} placeholder="Terrain" />
130
+ ```
131
+
132
+ ### Disabled control
133
+
134
+ ```tsx
135
+ const SPEED_OPTIONS = [
136
+ { value: 'standard', label: 'Standard' },
137
+ { value: 'priority', label: 'Priority' },
138
+ { value: 'overnight', label: 'Overnight', disabled: true },
139
+ ]
140
+
141
+ <Select label="Dispatch speed, unavailable" options={SPEED_OPTIONS} defaultValue="standard" disabled />
142
+ ```
143
+
144
+ ### Invalid
145
+
146
+ ```tsx
147
+ const SPEED_OPTIONS = [
148
+ { value: 'standard', label: 'Standard' },
149
+ { value: 'priority', label: 'Priority' },
150
+ { value: 'overnight', label: 'Overnight', disabled: true },
151
+ ]
152
+
153
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} invalid placeholder="Choose a speed" />
154
+ ```
155
+
156
+ ### Beside an Input
157
+
158
+ ```tsx
159
+ const TERRAIN_OPTIONS = [
160
+ { value: 'woodland', label: 'Woodland' },
161
+ { value: 'urban', label: 'Urban' },
162
+ { value: 'desert', label: 'Desert' },
163
+ { value: 'indoor', label: 'Indoor' },
164
+ ]
165
+
166
+ <label className="flex flex-col gap-1 text-sm">
167
+ Call sign
168
+ <Input placeholder="Anvil-3" />
169
+ </label>
170
+ {/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,
171
+ select, textarea, button among them — but Radix's trigger is a <button>, and a
172
+ <label> wrapping a button is inert: it neither names it nor forwards a click. The
173
+ Select's accessible name comes from its required `label` prop, and the visible text
174
+ here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would
175
+ have looked correct and done nothing. */}
176
+ <div className="flex flex-col gap-1 text-sm">
177
+ <span>Terrain</span>
178
+ <Select label="Terrain" options={TERRAIN_OPTIONS} placeholder="Choose terrain" />
179
+ </div>
180
+ ```
181
+
182
+ ## Keyboard
183
+
184
+ | Keys | Result |
185
+ |---|---|
186
+ | `Tab` | Moves focus to the trigger. |
187
+ | `Space` `Enter` | Opens the listbox. |
188
+ | `↑` `↓` | Moves the highlight. |
189
+ | `Home` `End` | Jumps to the first or last option. |
190
+ | `A–Z` | Typeahead: jumps to the first option that starts with the letters you type. |
191
+ | `Enter` | Selects the highlighted option and closes the list. |
192
+ | `Esc` | Closes without selecting and puts focus back on the trigger. |
193
+
194
+ ## Do
195
+
196
+ - Keep option labels short and written the same way.
197
+ - Use a placeholder that says what’s being chosen.
198
+ - Disable an unavailable option instead of removing it, so the list stops jumping around.
199
+ - Below about five options, use radios instead.
200
+ - Group rows under headings once the list mixes different kinds of thing, and keep each group to a handful.
201
+ - Give every row an icon, or none of them.
202
+ - Wrap it in a FormField for a visible label, help and error. FormField hands it the label.
203
+
204
+ ## Don't
205
+
206
+ - Don't build a searchable select out of this. That's the command palette's job.
207
+ - Don't use it for several values. That's MultiSelect, which shows each choice as a removable tag.
208
+ - Don't put more than a couple of dozen options in it without search.
209
+ - Don't make a group of one. A heading over a single row is just noise.
210
+ - Don't style the trigger differently from Input. A form with mismatched control heights looks broken.
211
+ - Don't bevel the trigger. It shares Input's frame and draws its own focus ring (the outline that shows which control the keyboard is on), and a cut corner would clip it. The listbox carries the crosshair instead: the shape lives on the surface, not on the control.
212
+ - Don't pass aria-label or aria-invalid through triggerProps. Both are typed away, not overridable, because label and invalid already decide them.
213
+
214
+ ## Anatomy
215
+
216
+ - **Trigger**: Uses exactly the same frame and styling as Input, so a select and a text field line up in a form.
217
+ - **Value**: Shows the placeholder until something’s chosen.
218
+ - **Content**: Portalled and positioned by Radix, the library of accessible building blocks this sits on. It slides down out of the trigger as it opens (M7; Radix’s Select closes at once, with no exit animation). It’s the one atom that carries the corner crosshair, because it’s a surface framing other content, and that’s what the crosshair marks. It’s also the shape that can’t be cut, since clip-path would clip the crosshair’s arms and the last row’s selection fill along with them.
219
+ - **Item**: Highlighted rows use signal.fill with signal.ink. That’s the fill-selection grammar, the system’s rule for how a selection looks.
220
+ - **ItemIndicator**: A tick on the selected row, so selection isn’t shown by the highlight alone.
221
+ - **Item icon**: Optional. Sits before the label and is aria-hidden, an ARIA (Accessible Rich Internet Applications) attribute that hides it from assistive tech. It follows the row colour, so it turns ink on the highlight. It isn’t shown in the trigger.
222
+ - **Group label**: Used with groups: a mono (monospaced font) heading over its rows that names the group for screen readers. It can’t be selected. A hairline separates the groups.
@@ -0,0 +1,143 @@
1
+ # Sheet
2
+
3
+ > A dialog that slides in from the side and runs the full height of the screen. Use it for filters, a menu on a narrow screen, or the details of one item next to a list.
4
+
5
+ - **Best for:** Filters, navigation on a narrow screen, or one item's details next to a list, sliding in from the side.
6
+ - **Not for:** A short confirmation: use Dialog or AlertDialog.
7
+ - Organism. Source: `src/components/organisms/sheet/Sheet.tsx`
8
+ - Rendered docs page: `/components/sheet`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Sheet, SheetClose, SheetContent, SheetTrigger } from 'src/components/organisms/sheet/Sheet'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `open (Sheet)` | `boolean` | | Whether it is open, when your code controls that (controlled mode). |
21
+ | `defaultOpen (Sheet)` | `boolean` | `false` | Whether it starts open, when the component tracks its own state (uncontrolled mode). |
22
+ | `onOpenChange (Sheet)` | `(open: boolean) => void` | | Called on every open and close, including Escape and a click on the scrim. |
23
+ | `asChild (Trigger)` | `boolean` | | Uses its single child as the trigger instead of adding its own button. Pass a Button. |
24
+ | `title (Content)` | `string` | | Required. The visible heading, and the name screen readers say. |
25
+ | `description (Content)` | `string` | | Shown under the title. Screen readers read it as the accessible description. |
26
+ | `side (Content)` | `'left' \| 'right'` | `'right'` | Which edge the sheet slides in from and stays attached to. |
27
+ | `closeLabel (Content)` | `string` | `'Close'` | What screen readers call the close button. |
28
+ | `className (Content)` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
29
+ | `ref (Content)` | `Ref<HTMLDivElement>` | | The element with the role dialog. |
30
+ | `ref (Trigger)` | `Ref<HTMLButtonElement>` | | The trigger button. |
31
+ | `asChild (Close)` | `boolean` | | Uses its single child as a control that closes the sheet. |
32
+
33
+ ## Examples
34
+
35
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
36
+
37
+ ### Filters on the right
38
+
39
+ ```tsx
40
+ function FiltersOnTheRightExample() {
41
+ const { toast } = useToast()
42
+ return (
43
+ <Sheet>
44
+ <SheetTrigger asChild>
45
+ <Button variant="secondary">Filter depots</Button>
46
+ </SheetTrigger>
47
+ <SheetContent title="Filters" description="Narrow the depot list.">
48
+ <fieldset className="flex flex-col gap-3">
49
+ <legend className="font-mono text-xs uppercase tracking-[0.12em] text-[var(--color-text-label)] mb-2">
50
+ Region
51
+ </legend>
52
+ <label className="flex items-center gap-2 min-h-6">
53
+ <Checkbox /> North
54
+ </label>
55
+ <label className="flex items-center gap-2 min-h-6">
56
+ <Checkbox /> South
57
+ </label>
58
+ <label className="flex items-center gap-2 min-h-6">
59
+ <Checkbox /> East
60
+ </label>
61
+ <label className="flex items-center gap-2 min-h-6">
62
+ <Checkbox /> West
63
+ </label>
64
+ </fieldset>
65
+ <div className="flex flex-wrap gap-3 mt-6">
66
+ <Button
67
+ variant="secondary"
68
+ onClick={() =>
69
+ toast({
70
+ title: 'Filters saved',
71
+ description: 'New depot lists open with these regions.',
72
+ tone: 'ok',
73
+ })
74
+ }
75
+ >
76
+ Save as default
77
+ </Button>
78
+ <SheetClose asChild>
79
+ <Button>Apply</Button>
80
+ </SheetClose>
81
+ </div>
82
+ </SheetContent>
83
+ </Sheet>
84
+ )
85
+ }
86
+
87
+ <FiltersOnTheRightExample />
88
+ ```
89
+
90
+ ### From the left
91
+
92
+ ```tsx
93
+ const DEPOTS = ['Brasov north', 'Brasov south', 'Sibiu', 'Cluj', 'Timisoara', 'Oradea']
94
+
95
+ function FromTheLeftExample() {
96
+ return (
97
+ <Sheet>
98
+ <SheetTrigger asChild>
99
+ <Button variant="secondary">Open depot list</Button>
100
+ </SheetTrigger>
101
+ <SheetContent side="left" title="Depots">
102
+ <ul role="list" className="flex flex-col gap-2">
103
+ {DEPOTS.map(depot => (
104
+ <li key={depot}>{depot}</li>
105
+ ))}
106
+ </ul>
107
+ </SheetContent>
108
+ </Sheet>
109
+ )
110
+ }
111
+
112
+ <FromTheLeftExample />
113
+ ```
114
+
115
+ ## Keyboard
116
+
117
+ | Keys | Result |
118
+ |---|---|
119
+ | `Tab` | Moves to the next control inside the sheet, looping from the last back to the first. |
120
+ | `Shift` `Tab` | Moves back, looping from the first to the last. |
121
+ | `Esc` | Closes it and sends focus back to whatever opened it. |
122
+
123
+ ## Do
124
+
125
+ - Use the right side for tools that act on the page, and the left for navigation.
126
+ - Keep the apply action at the end of the body, which is where a keyboard user gets to last.
127
+ - Close it after a choice that goes to another page.
128
+ - Show a toast from inside a sheet when it helps. It appears inside the sheet, screen readers can reach it, and Escape still closes the sheet.
129
+
130
+ ## Don't
131
+
132
+ - Don't put a second sheet on top of a sheet.
133
+ - Don't use a sheet for a two-line confirmation. That's a dialog.
134
+ - Don't hide the only way to reach a page inside a sheet on wide screens.
135
+
136
+ ## Anatomy
137
+
138
+ - **Scrim**: The dimmed layer over the page, coloured surface.scrim. The page under it is hidden from screen readers and can't take focus. Clicking the scrim closes the sheet.
139
+ - **Surface**: surface.base with a strong hairline border on its inner edge. It's full height, and 24rem wide or the width of the viewport (the visible browser area) minus 2rem. A rem is a unit based on the root text size, usually 16 pixels. Its motion is M7: when it closes, it slides back off its own edge while the scrim fades.
140
+ - **Title**: Required. An h2 (a second-level heading), and the sheet's accessible name, the name screen readers say for it.
141
+ - **Description**: Optional. The accessible description, which screen readers read after the name.
142
+ - **Body**: The part that scrolls. When it is taller than the screen it scrolls on its own, while the title and close button stay put.
143
+ - **Close**: A ghost icon button (no fill, just the icon) in the top right. It comes last when you move through with Tab.
@@ -0,0 +1,191 @@
1
+ # Sidebar
2
+
3
+ > The navigation down the side of an app. It holds groups of pages, each page with an icon and an optional count. Groups can fold away, and there is room for a header and an account line.
4
+
5
+ - **Best for:** App navigation down the side: grouped pages with icons and counts.
6
+ - **Not for:** A site header with a few links: use Navbar.
7
+ - Organism. Source: `src/components/organisms/sidebar/Sidebar.tsx`
8
+ - Rendered docs page: `/components/sidebar`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Sidebar } from 'src/components/organisms/sidebar/Sidebar'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `label` | `string` | | Required. The name screen readers give the navigation region. |
21
+ | `groups` | `SidebarGroup[]` | | Required. Each group has title, icon, items, collapsible and defaultOpen. |
22
+ | `header` | `ReactNode` | | Sits above the groups: a product name, a workspace switcher. |
23
+ | `footer` | `ReactNode` | | Pinned below the groups when the Sidebar has a height (h-full in a column layout): the signed-in account, a help link. |
24
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Pass your router's link component here. Map href to its own prop, and pass className, aria-current and onClick through. |
25
+ | `className` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
26
+ | `ref` | `Ref<HTMLElement>` | | The nav element. |
27
+
28
+ ## Types and helpers
29
+
30
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
31
+
32
+ ```ts
33
+ export type SidebarItem = {
34
+ label: string
35
+ href: string
36
+ /** An icon, drawn aria-hidden; the label carries the meaning. */
37
+ icon?: ReactNode
38
+ current?: boolean
39
+ /** A number shown at the end of the item and read as part of its name ("Orders 12"). */
40
+ count?: number
41
+ }
42
+ ```
43
+
44
+ ```ts
45
+ export type SidebarGroup = {
46
+ /** Also the React key for this group; unique within a Sidebar. */
47
+ title: string
48
+ /** Drawn before the title, aria-hidden; the title alone names the group. */
49
+ icon?: ReactNode
50
+ items: SidebarItem[]
51
+ /** @default true */
52
+ collapsible?: boolean
53
+ /** Ignored when the group holds the current item, which always starts open. @default true */
54
+ defaultOpen?: boolean
55
+ }
56
+ ```
57
+
58
+ ## Examples
59
+
60
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
61
+
62
+ ### Dashboard navigation
63
+
64
+ ```tsx
65
+ const GROUPS: SidebarGroup[] = [
66
+ {
67
+ title: 'Operations',
68
+ icon: <Warehouse />,
69
+ items: [
70
+ { label: 'Overview', href: '#overview', icon: <LayoutDashboard /> },
71
+ { label: 'Orders', href: '#orders', icon: <Package />, current: true, count: 12 },
72
+ { label: 'Shipments', href: '#shipments', icon: <Truck /> },
73
+ { label: 'Returns', href: '#returns', icon: <Inbox />, count: 3 },
74
+ ],
75
+ },
76
+ {
77
+ title: 'Catalogue',
78
+ icon: <Boxes />,
79
+ defaultOpen: false,
80
+ items: [
81
+ { label: 'Products', href: '#products', icon: <Box /> },
82
+ { label: 'Inventory', href: '#inventory', icon: <Map /> },
83
+ ],
84
+ },
85
+ {
86
+ title: 'Account',
87
+ icon: <UserCog />,
88
+ collapsible: false,
89
+ items: [
90
+ { label: 'Settings', href: '#settings', icon: <Settings /> },
91
+ { label: 'Team', href: '#team', icon: <Users /> },
92
+ ],
93
+ },
94
+ ]
95
+
96
+ const HEADER = <p className="font-display font-semibold">Hexline ops</p>
97
+
98
+ const FOOTER = <p className="text-xs text-[var(--color-text-secondary)]">Signed in as Ana Pop</p>
99
+
100
+ <div className="max-w-64">
101
+ <Sidebar label="Dashboard navigation" groups={GROUPS} header={HEADER} footer={FOOTER} />
102
+ </div>
103
+ ```
104
+
105
+ ### In a sheet on a narrow screen: picking a link closes the sheet, through renderLink
106
+
107
+ ```tsx
108
+ const GROUPS: SidebarGroup[] = [
109
+ {
110
+ title: 'Operations',
111
+ icon: <Warehouse />,
112
+ items: [
113
+ { label: 'Overview', href: '#overview', icon: <LayoutDashboard /> },
114
+ { label: 'Orders', href: '#orders', icon: <Package />, current: true, count: 12 },
115
+ { label: 'Shipments', href: '#shipments', icon: <Truck /> },
116
+ { label: 'Returns', href: '#returns', icon: <Inbox />, count: 3 },
117
+ ],
118
+ },
119
+ {
120
+ title: 'Catalogue',
121
+ icon: <Boxes />,
122
+ defaultOpen: false,
123
+ items: [
124
+ { label: 'Products', href: '#products', icon: <Box /> },
125
+ { label: 'Inventory', href: '#inventory', icon: <Map /> },
126
+ ],
127
+ },
128
+ {
129
+ title: 'Account',
130
+ icon: <UserCog />,
131
+ collapsible: false,
132
+ items: [
133
+ { label: 'Settings', href: '#settings', icon: <Settings /> },
134
+ { label: 'Team', href: '#team', icon: <Users /> },
135
+ ],
136
+ },
137
+ ]
138
+
139
+ const HEADER = <p className="font-display font-semibold">Hexline ops</p>
140
+
141
+ const FOOTER = <p className="text-xs text-[var(--color-text-secondary)]">Signed in as Ana Pop</p>
142
+
143
+ const closeOnChoose = ({ onClick, ...props }: LinkRenderProps) => (
144
+ <SheetClose asChild type={undefined}>
145
+ <a {...props} onClick={onClick} />
146
+ </SheetClose>
147
+ )
148
+
149
+ <Sheet>
150
+ <SheetTrigger asChild>
151
+ <Button variant="secondary">Open navigation</Button>
152
+ </SheetTrigger>
153
+ <SheetContent side="left" title="Navigation">
154
+ <Sidebar
155
+ label="Dashboard navigation, in a sheet"
156
+ groups={GROUPS}
157
+ header={HEADER}
158
+ footer={FOOTER}
159
+ renderLink={closeOnChoose}
160
+ />
161
+ </SheetContent>
162
+ </Sheet>
163
+ ```
164
+
165
+ ## Keyboard
166
+
167
+ | Keys | Result |
168
+ |---|---|
169
+ | `Tab` | Moves through each group title that can fold, then each visible item. |
170
+ | `Enter` | Follows an item, or folds and unfolds a group. |
171
+ | `Space` | Folds and unfolds a group. |
172
+
173
+ ## Do
174
+
175
+ - Only show counts for things that need attention: open orders, not total products.
176
+ - Keep the group that holds the current page open.
177
+ - On a narrow screen, put the whole sidebar in a Sheet behind a button.
178
+
179
+ ## Don't
180
+
181
+ - Don't nest groups inside groups.
182
+ - Don't show a count of zero. Leave it off.
183
+ - Don't rely on icons alone. Every item keeps its label.
184
+
185
+ ## Anatomy
186
+
187
+ - **nav**: The navigation region. Screen readers call it by the label prop.
188
+ - **Group title**: A disclosure button (one that shows and hides the list) when the group can fold, plain text when it can't. It's text.secondary, so it still reads on a raised panel. Its list opens and closes by animating its height. Opening also fades and slides each item in, each one a little later than the one before (capped, so a long group never feels slow). Closing fades everything out together, all at once. If someone has asked their system for less motion (prefers-reduced-motion: reduce), both happen instantly.
189
+ - **Item**: An icon, a label and an optional count. The current page gets the fill, plus aria-current, an ARIA attribute (Accessible Rich Internet Applications, the labels screen readers rely on) that tells screen readers this is the page you are on.
190
+ - **Header**: Sits above the groups: a product name, a workspace switcher.
191
+ - **Footer**: Pinned below the groups: the signed-in account, a help link.