division-ds 0.1.2 → 0.2.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.
Files changed (98) hide show
  1. package/AGENTS.md +23 -3
  2. package/README.md +11 -0
  3. package/division-ds-0.2.1.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +207 -0
  6. package/public/docs/components/ai-label.md +90 -0
  7. package/public/docs/components/alert-dialog.md +292 -0
  8. package/public/docs/components/alert.md +175 -0
  9. package/public/docs/components/approval-request.md +148 -0
  10. package/public/docs/components/auth-form.md +132 -0
  11. package/public/docs/components/avatar-group.md +152 -0
  12. package/public/docs/components/avatar.md +179 -0
  13. package/public/docs/components/badge.md +178 -0
  14. package/public/docs/components/breadcrumbs.md +112 -0
  15. package/public/docs/components/button.md +204 -0
  16. package/public/docs/components/card.md +257 -0
  17. package/public/docs/components/carousel.md +130 -0
  18. package/public/docs/components/cart.md +172 -0
  19. package/public/docs/components/chart-panel.md +358 -0
  20. package/public/docs/components/chat-message.md +157 -0
  21. package/public/docs/components/chat-thread.md +244 -0
  22. package/public/docs/components/checkbox.md +149 -0
  23. package/public/docs/components/checkout-form.md +128 -0
  24. package/public/docs/components/command-palette.md +147 -0
  25. package/public/docs/components/data-table.md +401 -0
  26. package/public/docs/components/description-list.md +103 -0
  27. package/public/docs/components/dialog.md +197 -0
  28. package/public/docs/components/divider.md +97 -0
  29. package/public/docs/components/dropdown-menu.md +222 -0
  30. package/public/docs/components/empty-state.md +145 -0
  31. package/public/docs/components/footer.md +397 -0
  32. package/public/docs/components/form-field.md +216 -0
  33. package/public/docs/components/icon-button.md +127 -0
  34. package/public/docs/components/input.md +200 -0
  35. package/public/docs/components/kanban-board.md +300 -0
  36. package/public/docs/components/kbd.md +72 -0
  37. package/public/docs/components/link.md +98 -0
  38. package/public/docs/components/list-item.md +241 -0
  39. package/public/docs/components/loading-animation.md +72 -0
  40. package/public/docs/components/multi-select.md +202 -0
  41. package/public/docs/components/navbar.md +144 -0
  42. package/public/docs/components/newsletter-form.md +112 -0
  43. package/public/docs/components/page-header.md +151 -0
  44. package/public/docs/components/pagination.md +212 -0
  45. package/public/docs/components/product-card.md +172 -0
  46. package/public/docs/components/progress.md +101 -0
  47. package/public/docs/components/prompt-input.md +151 -0
  48. package/public/docs/components/quantity-stepper.md +140 -0
  49. package/public/docs/components/radio.md +177 -0
  50. package/public/docs/components/rating.md +106 -0
  51. package/public/docs/components/score-ring.md +95 -0
  52. package/public/docs/components/search.md +116 -0
  53. package/public/docs/components/section-heading.md +113 -0
  54. package/public/docs/components/segmented-control.md +126 -0
  55. package/public/docs/components/select.md +224 -0
  56. package/public/docs/components/sheet.md +145 -0
  57. package/public/docs/components/sidebar.md +193 -0
  58. package/public/docs/components/skeleton.md +135 -0
  59. package/public/docs/components/slider.md +114 -0
  60. package/public/docs/components/sparkline.md +128 -0
  61. package/public/docs/components/spinner.md +98 -0
  62. package/public/docs/components/stat.md +106 -0
  63. package/public/docs/components/status-tracker.md +145 -0
  64. package/public/docs/components/stepper.md +147 -0
  65. package/public/docs/components/switch.md +153 -0
  66. package/public/docs/components/tabs.md +180 -0
  67. package/public/docs/components/tag.md +105 -0
  68. package/public/docs/components/textarea.md +148 -0
  69. package/public/docs/components/timeline.md +152 -0
  70. package/public/docs/components/toast.md +194 -0
  71. package/public/docs/components/tool-call.md +107 -0
  72. package/public/docs/components/tooltip.md +108 -0
  73. package/public/manifest/components.json +198 -15
  74. package/public/manifest/tokens.json +75 -5
  75. package/scripts/division-ds.mjs +78 -42
  76. package/scripts/lib/agent-docs.mjs +80 -6
  77. package/scripts/lib/ai-copy.mjs +120 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +57 -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 +116 -0
  83. package/scripts/lib/mcp-tools.mjs +266 -0
  84. package/scripts/lib/props-validator.mjs +3 -2
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/scripts/lib/token-discipline.mjs +10 -3
  87. package/skills/division-ds/SKILL.md +90 -0
  88. package/skills/division-ds/commands/add.md +25 -0
  89. package/skills/division-ds/commands/build.md +46 -0
  90. package/skills/division-ds/commands/fix.md +28 -0
  91. package/skills/division-ds/commands/review.md +25 -0
  92. package/skills/division-ds/topics/ai.md +8 -0
  93. package/skills/division-ds/topics/colour.md +11 -0
  94. package/skills/division-ds/topics/feedback.md +9 -0
  95. package/skills/division-ds/topics/forms.md +22 -0
  96. package/skills/division-ds/topics/layout.md +36 -0
  97. package/skills/division-ds/topics/motion.md +10 -0
  98. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,113 @@
1
+ # SectionHeading
2
+
3
+ > The heading that opens a section of a page: a title under a hairline, with an optional description and an optional action. The page tells it which heading level to use, because only the page knows its own outline.
4
+
5
+ - **Best for:** Opening a section of a page, at the level that carries on the outline.
6
+ - **Not for:** The page title: use PageHeader.
7
+ - Molecule. Source: `src/components/molecules/section-heading/SectionHeading.tsx`
8
+ - Rendered docs page: `/components/section-heading`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { SectionHeading } from 'src/components/molecules/section-heading/SectionHeading'
14
+ ```
15
+
16
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `title` | `string` | | Required. The heading text. |
23
+ | `level` | `2 \| 3 \| 4` | | Required, with no default. The heading level the page outline needs at this point. Leave it out and the code won't compile. |
24
+ | `description` | `ReactNode` | | One or two sentences under the title. |
25
+ | `action` | `ReactNode` | | A single control at the end of the row, usually a link to the full list. |
26
+ | `index` | `number` | | The section's position in a numbered run of sections, drawn above the title. It's decoration and never part of the heading's name. |
27
+ | `id` | `string` | | Goes on the heading element, not the wrapper, so a section can point aria-labelledby at it (that's how the section gets its name for screen readers). |
28
+ | `className` | `string` | | Added to the root. Where Tailwind utility classes clash, the later one wins. |
29
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root element. |
30
+
31
+ ## Examples
32
+
33
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
34
+
35
+ ### Title only
36
+
37
+ ```tsx
38
+ <SectionHeading level={4} title="Top-rated replicas" />
39
+ ```
40
+
41
+ ### With a description and an action
42
+
43
+ ```tsx
44
+ <SectionHeading
45
+ level={4}
46
+ title="Top-rated accessories"
47
+ description="Optics and parts, ordered by what the reviews say."
48
+ action={<Link href="#accessories">All accessories</Link>}
49
+ />
50
+ ```
51
+
52
+ ### Numbered, in a run
53
+
54
+ ```tsx
55
+ <div className="flex flex-col gap-10">
56
+ <SectionHeading level={4} index={1} title="Replicas" description="Three lengths on one receiver." />
57
+ <SectionHeading level={4} index={2} title="Optics" description="A tube sight and a holo sight." />
58
+ <SectionHeading
59
+ level={4}
60
+ index={3}
61
+ title="Parts and accessories"
62
+ action={
63
+ <Button asChild variant="secondary" size="sm">
64
+ <a href="#parts">All parts</a>
65
+ </Button>
66
+ }
67
+ />
68
+ </div>
69
+ ```
70
+
71
+ ### Naming its section
72
+
73
+ ```tsx
74
+ <section aria-labelledby="section-heading-docs-care">
75
+ <SectionHeading
76
+ level={4}
77
+ id="section-heading-docs-care"
78
+ title="Care plans"
79
+ description="The section below is a region named “Care plans”. It takes that name from this heading, which carries the id."
80
+ />
81
+ <p className="mt-4 font-body text-sm text-[var(--color-text-primary)]">
82
+ A chrono check, a tune and upgrade fitting, billed monthly.
83
+ </p>
84
+ </section>
85
+ ```
86
+
87
+ ## Keyboard
88
+
89
+ | Keys | Result |
90
+ |---|---|
91
+ | `Tab` | Moves to the action, if there is one. The heading itself isn't a Tab stop. |
92
+
93
+ ## Do
94
+
95
+ - Pass the level that carries on the page outline: h2 under the page h1, h3 inside an h2 section.
96
+ - Give the heading an id and point the section at it with aria-labelledby, so the region has a name.
97
+ - Name the action after where it goes: "All replicas", not "View all" four times on one page.
98
+ - Number every section in a run, or none of them.
99
+
100
+ ## Don't
101
+
102
+ - Don't use it as the page title. A page has one h1, and that isn't a section opener.
103
+ - Don't pick a level for its size. The size is the same at every level; the level is about the outline.
104
+ - Don't put more than one control in the action slot. A second one belongs in the section body.
105
+ - Don't stack one straight under another. A section heading opens content, not a second heading.
106
+
107
+ ## Anatomy
108
+
109
+ - **Rule**: A hairline along the top, in border.hairline-strong. It marks where the section starts.
110
+ - **Index**: Optional. A two-digit number in monospace type, on its own line above the title, and aria-hidden so screen readers skip it. The leading zero is in numeral.pad and the digit in accent.signal.text. It sits above the title, not in front, so the title keeps the same left edge as its description and its section.
111
+ - **Title**: The heading element, h2 to h4 (heading levels 2 to 4). Oxanium, uppercase, 20px (pixels), going up to 24px when the heading has 42rem or more of width.
112
+ - **Description**: Optional. Body text in text.secondary, at most 60 characters a line.
113
+ - **Action**: Optional. One control at the far end of the row. It drops under the text when there isn't room.
@@ -0,0 +1,126 @@
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
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `label` | `string` | | Required. Names the group: "View", "Range". |
23
+ | `options` | `readonly SegmentedControlOption[]` | | Required. Two to five. |
24
+ | `value` | `string` | | Required. The selected option. |
25
+ | `onValueChange` | `(value: string) => void` | | Required. Called with the new value. |
26
+ | `size` | `'sm' \| 'md'` | `'md'` | The same height as Button's sm or md. |
27
+ | `className` | `string` | | Added to the group. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the group. |
29
+ | `value (SegmentedControlOption)` | `string` | | Required. What onValueChange receives. |
30
+ | `label (SegmentedControlOption)` | `string` | | Required. Always shown. |
31
+ | `icon (SegmentedControlOption)` | `ReactNode` | | A lucide icon before the label, aria-hidden so screen readers skip it. |
32
+
33
+ ## Types and helpers
34
+
35
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
36
+
37
+ ```ts
38
+ export type SegmentedControlOption = {
39
+ value: string
40
+ /** Always shown. The icon, if any, sits before it. */
41
+ label: string
42
+ /** A lucide icon, aria-hidden: the label names the option. */
43
+ icon?: ReactNode
44
+ }
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
50
+
51
+ ### A time range
52
+
53
+ ```tsx
54
+ const RANGES = [
55
+ { value: '1h', label: '1 h' },
56
+ { value: '24h', label: '24 h' },
57
+ { value: '7d', label: '7 d' },
58
+ { value: '30d', label: '30 d' },
59
+ ]
60
+
61
+ function RangeExample() {
62
+ const [range, setRange] = useState('24h')
63
+ return (
64
+ <div className="flex flex-col items-center gap-3">
65
+ <SegmentedControl label="Range" options={RANGES} value={range} onValueChange={setRange} />
66
+ <p className="font-body text-sm text-[var(--color-text-secondary)]">Showing the last {RANGES.find(option => option.value === range)?.label}.</p>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ <RangeExample />
72
+ ```
73
+
74
+ ### Small, with icons, lined up with a small Button
75
+
76
+ ```tsx
77
+ function ToolbarExample() {
78
+ const [view, setView] = useState('board')
79
+ return (
80
+ <div className="flex flex-wrap items-center gap-3">
81
+ <SegmentedControl
82
+ label="View"
83
+ size="sm"
84
+ options={[
85
+ { value: 'board', label: 'Board', icon: <Columns3 /> },
86
+ { value: 'table', label: 'Table', icon: <Table /> },
87
+ ]}
88
+ value={view}
89
+ onValueChange={setView}
90
+ />
91
+ <Button size="sm" variant="secondary">
92
+ Export
93
+ </Button>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ <ToolbarExample />
99
+ ```
100
+
101
+ ## Keyboard
102
+
103
+ | Keys | Result |
104
+ |---|---|
105
+ | `Tab` | Moves into the group, onto the selected option, and out again. |
106
+ | `ArrowLeft` `ArrowRight` | Moves to the previous or next option and selects it. Wraps round at the ends. |
107
+ | `ArrowUp` `ArrowDown` | The same as Left and Right. |
108
+ | `Space` | Selects the focused option, if it isn't selected already. |
109
+
110
+ ## Do
111
+
112
+ - Use it where the change happens straight away and can be undone: the view, a time range, a unit.
113
+ - Keep labels to a word or two; they're set in uppercase monospace.
114
+ - Match the size to the buttons beside it: sm in a busy toolbar, md otherwise.
115
+
116
+ ## Don't
117
+
118
+ - Don't use it for panels of different content; that's Tabs, which links each tab to its panel.
119
+ - Don't use it for a choice that's sent with a form; that's a RadioGroup inside the form.
120
+ - Don't drop the label. Every option keeps its words, even with an icon.
121
+
122
+ ## Anatomy
123
+
124
+ - **Group**: A radiogroup named by label, framed with the control hairline. It scrolls sideways if it's too wide.
125
+ - **Option**: A radio with a monospace label and an optional icon, the same height as a Button at sm or md.
126
+ - **Selected option**: Signal fill, ink text, semibold. Its focus ring is drawn in ink.
@@ -0,0 +1,224 @@
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
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Requires
19
+
20
+ - 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.
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ |---|---|---|---|
26
+ | `label` | `string` | | Required. The trigger’s accessible name, the name a screen reader announces for it. |
27
+ | `options` | `SelectOptionWithIcon[]` | | { value, label, disabled?, icon? } rows shown in the listbox. Pass this or groups, never both. |
28
+ | `groups` | `SelectGroup[]` | | { label, options }[]: rows under headings. Pass this or options, never both. |
29
+ | `placeholder` | `string` | `'Select…'` | Shown until a value is chosen. |
30
+ | `value` | `string` | | The selection, when you control it from outside. Following Radix’s own convention, pass it together with onValueChange. |
31
+ | `defaultValue` | `string` | | The starting selection, when the select manages it itself (uncontrolled). Pass this or value, never both. |
32
+ | `onValueChange` | `(value: string) => void` | | Fires with the new value when a row is chosen, whether the select is controlled or uncontrolled. |
33
+ | `disabled` | `boolean` | | Disables the whole control. |
34
+ | `required` | `boolean` | | Marks the control as required for form submission. |
35
+ | `name` | `string` | | The form field name, for native form submission. |
36
+ | `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Matches Input’s sizes so the two line up. |
37
+ | `invalid` | `boolean` | `false` | Sets aria-invalid, which tells assistive tech the field has an error, and shows the invalid border. |
38
+ | `className` | `string` | | Merged with the trigger’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
39
+ | `ref` | `Ref<HTMLButtonElement>` | | Passed through to the trigger button. |
40
+ | `id` | `string` | | The trigger’s id. FormField sets it. |
41
+ | `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. |
42
+ | `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. |
43
+ | `...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. |
44
+
45
+ ## Types and helpers
46
+
47
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
48
+
49
+ ```ts
50
+ export type SelectOption = { value: string; label: string; disabled?: boolean }
51
+ ```
52
+
53
+ ```ts
54
+ /** A Select row: a `SelectOption` plus an optional leading icon. Kept as its own type rather than
55
+ * an `icon` field on `SelectOption`, because MultiSelect takes `SelectOption[]` too and renders
56
+ * no icons: widening the shared type would let an icon compile there and vanish. */
57
+ export type SelectOptionWithIcon = SelectOption & {
58
+ /** Leading glyph, aria-hidden: the label names the row. Not repeated in the trigger. */
59
+ icon?: ReactNode
60
+ }
61
+ ```
62
+
63
+ ```ts
64
+ /** A titled run of rows. The heading is not selectable; it names the group for screen readers. */
65
+ export type SelectGroup = { label: string; options: SelectOptionWithIcon[] }
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
71
+
72
+ ### Placeholder
73
+
74
+ ```tsx
75
+ const SPEED_OPTIONS = [
76
+ { value: 'standard', label: 'Standard' },
77
+ { value: 'priority', label: 'Priority' },
78
+ { value: 'overnight', label: 'Overnight', disabled: true },
79
+ ]
80
+
81
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} placeholder="Choose a speed" />
82
+ ```
83
+
84
+ ### With a value
85
+
86
+ ```tsx
87
+ const SPEED_OPTIONS = [
88
+ { value: 'standard', label: 'Standard' },
89
+ { value: 'priority', label: 'Priority' },
90
+ { value: 'overnight', label: 'Overnight', disabled: true },
91
+ ]
92
+
93
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} defaultValue="priority" />
94
+ ```
95
+
96
+ ### Groups with headings and icons
97
+
98
+ ```tsx
99
+ const DELIVERY_GROUPS: SelectGroup[] = [
100
+ {
101
+ label: 'Courier',
102
+ options: [
103
+ { value: 'standard', label: 'Standard', icon: <Truck /> },
104
+ { value: 'priority', label: 'Priority', icon: <Zap /> },
105
+ ],
106
+ },
107
+ {
108
+ label: 'Collect',
109
+ options: [
110
+ { value: 'depot', label: 'Brasov depot', icon: <Package /> },
111
+ { value: 'field', label: 'At the field', icon: <Crosshair /> },
112
+ ],
113
+ },
114
+ ]
115
+
116
+ <Select label="Delivery" groups={DELIVERY_GROUPS} placeholder="Choose delivery" />
117
+ ```
118
+
119
+ ### Sizes
120
+
121
+ ```tsx
122
+ const TERRAIN_OPTIONS = [
123
+ { value: 'woodland', label: 'Woodland' },
124
+ { value: 'urban', label: 'Urban' },
125
+ { value: 'desert', label: 'Desert' },
126
+ { value: 'indoor', label: 'Indoor' },
127
+ ]
128
+
129
+ <Select label="Terrain (small)" inputSize="sm" options={TERRAIN_OPTIONS} placeholder="Terrain" />
130
+ <Select label="Terrain (medium)" inputSize="md" options={TERRAIN_OPTIONS} placeholder="Terrain" />
131
+ <Select label="Terrain (large)" inputSize="lg" options={TERRAIN_OPTIONS} placeholder="Terrain" />
132
+ ```
133
+
134
+ ### Disabled control
135
+
136
+ ```tsx
137
+ const SPEED_OPTIONS = [
138
+ { value: 'standard', label: 'Standard' },
139
+ { value: 'priority', label: 'Priority' },
140
+ { value: 'overnight', label: 'Overnight', disabled: true },
141
+ ]
142
+
143
+ <Select label="Dispatch speed, unavailable" options={SPEED_OPTIONS} defaultValue="standard" disabled />
144
+ ```
145
+
146
+ ### Invalid
147
+
148
+ ```tsx
149
+ const SPEED_OPTIONS = [
150
+ { value: 'standard', label: 'Standard' },
151
+ { value: 'priority', label: 'Priority' },
152
+ { value: 'overnight', label: 'Overnight', disabled: true },
153
+ ]
154
+
155
+ <Select label="Dispatch speed" options={SPEED_OPTIONS} invalid placeholder="Choose a speed" />
156
+ ```
157
+
158
+ ### Beside an Input
159
+
160
+ ```tsx
161
+ const TERRAIN_OPTIONS = [
162
+ { value: 'woodland', label: 'Woodland' },
163
+ { value: 'urban', label: 'Urban' },
164
+ { value: 'desert', label: 'Desert' },
165
+ { value: 'indoor', label: 'Indoor' },
166
+ ]
167
+
168
+ <label className="flex flex-col gap-1 text-sm">
169
+ Call sign
170
+ <Input placeholder="Anvil-3" />
171
+ </label>
172
+ {/* Deliberately NOT a <label>. A <label> associates with a labelable element — input,
173
+ select, textarea, button among them — but Radix's trigger is a <button>, and a
174
+ <label> wrapping a button is inert: it neither names it nor forwards a click. The
175
+ Select's accessible name comes from its required `label` prop, and the visible text
176
+ here matches it exactly (SC 2.5.3, label in name). Wrapping it in a <label> would
177
+ have looked correct and done nothing. */}
178
+ <div className="flex flex-col gap-1 text-sm">
179
+ <span>Terrain</span>
180
+ <Select label="Terrain" options={TERRAIN_OPTIONS} placeholder="Choose terrain" />
181
+ </div>
182
+ ```
183
+
184
+ ## Keyboard
185
+
186
+ | Keys | Result |
187
+ |---|---|
188
+ | `Tab` | Moves focus to the trigger. |
189
+ | `Space` `Enter` | Opens the listbox. |
190
+ | `↑` `↓` | Moves the highlight. |
191
+ | `Home` `End` | Jumps to the first or last option. |
192
+ | `A–Z` | Typeahead: jumps to the first option that starts with the letters you type. |
193
+ | `Enter` | Selects the highlighted option and closes the list. |
194
+ | `Esc` | Closes without selecting and puts focus back on the trigger. |
195
+
196
+ ## Do
197
+
198
+ - Keep option labels short and written the same way.
199
+ - Use a placeholder that says what’s being chosen.
200
+ - Disable an unavailable option instead of removing it, so the list stops jumping around.
201
+ - Below about five options, use radios instead.
202
+ - Group rows under headings once the list mixes different kinds of thing, and keep each group to a handful.
203
+ - Give every row an icon, or none of them.
204
+ - Wrap it in a FormField for a visible label, help and error. FormField hands it the label.
205
+
206
+ ## Don't
207
+
208
+ - Don't build a searchable select out of this. That's the command palette's job.
209
+ - Don't use it for several values. That's MultiSelect, which shows each choice as a removable tag.
210
+ - Don't put more than a couple of dozen options in it without search.
211
+ - Don't make a group of one. A heading over a single row is just noise.
212
+ - Don't style the trigger differently from Input. A form with mismatched control heights looks broken.
213
+ - 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.
214
+ - Don't pass aria-label or aria-invalid through triggerProps. Both are typed away, not overridable, because label and invalid already decide them.
215
+
216
+ ## Anatomy
217
+
218
+ - **Trigger**: Uses exactly the same frame and styling as Input, so a select and a text field line up in a form.
219
+ - **Value**: Shows the placeholder until something’s chosen.
220
+ - **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.
221
+ - **Item**: Highlighted rows use accent.signal.indicator with accent.signal.indicator-ink. That’s the fill-selection grammar, the system’s rule for how a selection looks.
222
+ - **ItemIndicator**: A tick on the selected row, so selection isn’t shown by the highlight alone.
223
+ - **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.
224
+ - **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,145 @@
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
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `open (Sheet)` | `boolean` | | Whether it is open, when your code controls that (controlled mode). |
23
+ | `defaultOpen (Sheet)` | `boolean` | `false` | Whether it starts open, when the component tracks its own state (uncontrolled mode). |
24
+ | `onOpenChange (Sheet)` | `(open: boolean) => void` | | Called on every open and close, including Escape and a click on the scrim. |
25
+ | `asChild (Trigger)` | `boolean` | | Uses its single child as the trigger instead of adding its own button. Pass a Button. |
26
+ | `title (Content)` | `string` | | Required. The visible heading, and the name screen readers say. |
27
+ | `description (Content)` | `string` | | Shown under the title. Screen readers read it as the accessible description. |
28
+ | `side (Content)` | `'left' \| 'right'` | `'right'` | Which edge the sheet slides in from and stays attached to. |
29
+ | `closeLabel (Content)` | `string` | `'Close'` | What screen readers call the close button. |
30
+ | `className (Content)` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
31
+ | `ref (Content)` | `Ref<HTMLDivElement>` | | The element with the role dialog. |
32
+ | `ref (Trigger)` | `Ref<HTMLButtonElement>` | | The trigger button. |
33
+ | `asChild (Close)` | `boolean` | | Uses its single child as a control that closes the sheet. |
34
+
35
+ ## Examples
36
+
37
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
38
+
39
+ ### Filters on the right
40
+
41
+ ```tsx
42
+ function FiltersOnTheRightExample() {
43
+ const { toast } = useToast()
44
+ return (
45
+ <Sheet>
46
+ <SheetTrigger asChild>
47
+ <Button variant="secondary">Filter depots</Button>
48
+ </SheetTrigger>
49
+ <SheetContent title="Filters" description="Narrow the depot list.">
50
+ <fieldset className="flex flex-col gap-3">
51
+ <legend className="font-mono text-xs uppercase tracking-[0.12em] text-[var(--color-text-label)] mb-2">
52
+ Region
53
+ </legend>
54
+ <label className="flex items-center gap-2 min-h-6">
55
+ <Checkbox /> North
56
+ </label>
57
+ <label className="flex items-center gap-2 min-h-6">
58
+ <Checkbox /> South
59
+ </label>
60
+ <label className="flex items-center gap-2 min-h-6">
61
+ <Checkbox /> East
62
+ </label>
63
+ <label className="flex items-center gap-2 min-h-6">
64
+ <Checkbox /> West
65
+ </label>
66
+ </fieldset>
67
+ <div className="flex flex-wrap gap-3 mt-6">
68
+ <Button
69
+ variant="secondary"
70
+ onClick={() =>
71
+ toast({
72
+ title: 'Filters saved',
73
+ description: 'New depot lists open with these regions.',
74
+ tone: 'ok',
75
+ })
76
+ }
77
+ >
78
+ Save as default
79
+ </Button>
80
+ <SheetClose asChild>
81
+ <Button>Apply</Button>
82
+ </SheetClose>
83
+ </div>
84
+ </SheetContent>
85
+ </Sheet>
86
+ )
87
+ }
88
+
89
+ <FiltersOnTheRightExample />
90
+ ```
91
+
92
+ ### From the left
93
+
94
+ ```tsx
95
+ const DEPOTS = ['Brasov north', 'Brasov south', 'Sibiu', 'Cluj', 'Timisoara', 'Oradea']
96
+
97
+ function FromTheLeftExample() {
98
+ return (
99
+ <Sheet>
100
+ <SheetTrigger asChild>
101
+ <Button variant="secondary">Open depot list</Button>
102
+ </SheetTrigger>
103
+ <SheetContent side="left" title="Depots">
104
+ <ul role="list" className="flex flex-col gap-2">
105
+ {DEPOTS.map(depot => (
106
+ <li key={depot}>{depot}</li>
107
+ ))}
108
+ </ul>
109
+ </SheetContent>
110
+ </Sheet>
111
+ )
112
+ }
113
+
114
+ <FromTheLeftExample />
115
+ ```
116
+
117
+ ## Keyboard
118
+
119
+ | Keys | Result |
120
+ |---|---|
121
+ | `Tab` | Moves to the next control inside the sheet, looping from the last back to the first. |
122
+ | `Shift` `Tab` | Moves back, looping from the first to the last. |
123
+ | `Esc` | Closes it and sends focus back to whatever opened it. |
124
+
125
+ ## Do
126
+
127
+ - Use the right side for tools that act on the page, and the left for navigation.
128
+ - Keep the apply action at the end of the body, which is where a keyboard user gets to last.
129
+ - Close it after a choice that goes to another page.
130
+ - 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.
131
+
132
+ ## Don't
133
+
134
+ - Don't put a second sheet on top of a sheet.
135
+ - Don't use a sheet for a two-line confirmation. That's a dialog.
136
+ - Don't hide the only way to reach a page inside a sheet on wide screens.
137
+
138
+ ## Anatomy
139
+
140
+ - **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.
141
+ - **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.
142
+ - **Title**: Required. An h2 (a second-level heading), and the sheet's accessible name, the name screen readers say for it.
143
+ - **Description**: Optional. The accessible description, which screen readers read after the name.
144
+ - **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.
145
+ - **Close**: A ghost icon button (no fill, just the icon) in the top right. It comes last when you move through with Tab.