division-ds 0.1.2 → 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 +182 -6
  74. package/public/manifest/tokens.json +39 -0
  75. package/scripts/division-ds.mjs +74 -38
  76. package/scripts/lib/agent-docs.mjs +53 -1
  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,195 @@
1
+ # Dialog
2
+
3
+ > A panel for one focused task, shown on top of the page. The page dims and goes inert, meaning it can't be clicked or focused. Keyboard focus moves inside the dialog and stays there. Escape or the close button hands focus back to whatever opened the dialog.
4
+
5
+ - **Best for:** A focused task on top of the page, such as an edit form.
6
+ - **Not for:** A destructive confirm: use AlertDialog. A message that doesn't need a decision: use Toast.
7
+ - Organism. Source: `src/components/organisms/dialog/Dialog.tsx`
8
+ - Rendered docs page: `/components/dialog`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTrigger } from 'src/components/organisms/dialog/Dialog'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `open (Dialog)` | `boolean` | | Whether it is open, when your code controls that (controlled mode). |
21
+ | `defaultOpen (Dialog)` | `boolean` | `false` | Whether it starts open, when the component tracks its own state (uncontrolled mode). |
22
+ | `onOpenChange (Dialog)` | `(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
+ | `size (Content)` | `'sm' \| 'md' \| 'lg'` | `'md'` | Maximum width: 24rem, 32rem or 42rem (rem is a unit based on the root text size, usually 16 pixels). |
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 dialog. |
32
+ | `className (Footer)` | `string` | | Added to the root classes. Where two Tailwind utility classes clash, the later one wins. |
33
+
34
+ ## Examples
35
+
36
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
37
+
38
+ ### A form in a dialog
39
+
40
+ ```tsx
41
+ function FormInDialogExample() {
42
+ return (
43
+ <Dialog>
44
+ <DialogTrigger asChild>
45
+ <Button>Edit depot</Button>
46
+ </DialogTrigger>
47
+ <DialogContent title="Edit depot" description="Changes apply to every open manifest.">
48
+ <div className="flex flex-col gap-4">
49
+ <FormField label="Depot name">
50
+ <Input defaultValue="Brasov north" />
51
+ </FormField>
52
+ <FormField label="Notes">
53
+ <Textarea placeholder="Anything the driver should know." />
54
+ </FormField>
55
+ </div>
56
+ <DialogFooter>
57
+ <DialogClose asChild>
58
+ <Button variant="secondary">Cancel</Button>
59
+ </DialogClose>
60
+ <DialogClose asChild>
61
+ <Button>Save</Button>
62
+ </DialogClose>
63
+ </DialogFooter>
64
+ </DialogContent>
65
+ </Dialog>
66
+ )
67
+ }
68
+
69
+ <FormInDialogExample />
70
+ ```
71
+
72
+ ### Confirm a destructive action, opened without a trigger
73
+
74
+ ```tsx
75
+ function ArchiveExample() {
76
+ const [open, setOpen] = useState(false)
77
+ const [status, setStatus] = useState('')
78
+
79
+ return (
80
+ <div className="flex flex-col items-start gap-3">
81
+ <Button
82
+ variant="danger"
83
+ onClick={() => {
84
+ setStatus('')
85
+ setOpen(true)
86
+ }}
87
+ >
88
+ Archive manifest
89
+ </Button>
90
+ <Dialog open={open} onOpenChange={setOpen}>
91
+ <DialogContent title="Archive manifest?" size="sm">
92
+ {/* token-discipline: allow, a manifest number in copy, not a colour */}
93
+ <p>Manifest #482 moves to the archive. It stays recoverable there for 30 days.</p>
94
+ <DialogFooter>
95
+ <DialogClose asChild>
96
+ <Button variant="secondary">Keep it</Button>
97
+ </DialogClose>
98
+ <Button
99
+ variant="danger"
100
+ onClick={() => {
101
+ setStatus('Manifest archived.')
102
+ setOpen(false)
103
+ }}
104
+ >
105
+ Archive
106
+ </Button>
107
+ </DialogFooter>
108
+ </DialogContent>
109
+ </Dialog>
110
+ <p role="status" className={docsProse}>
111
+ {status}
112
+ </p>
113
+ </div>
114
+ )
115
+ }
116
+
117
+ <ArchiveExample />
118
+ ```
119
+
120
+ ### Long content
121
+
122
+ ```tsx
123
+ function LongContentExample() {
124
+ const contentRef = useRef<HTMLDivElement>(null)
125
+ return (
126
+ <Dialog>
127
+ <DialogTrigger asChild>
128
+ <Button variant="secondary">Read shipping terms</Button>
129
+ </DialogTrigger>
130
+ <DialogContent
131
+ ref={contentRef}
132
+ title="Shipping terms"
133
+ size="lg"
134
+ tabIndex={-1}
135
+ onOpenAutoFocus={event => {
136
+ event.preventDefault()
137
+ contentRef.current?.focus()
138
+ }}
139
+ >
140
+ <div className="flex flex-col gap-4">
141
+ <p>Orders placed before 14:00 local time ship the same day; anything after ships the next business day.</p>
142
+ <p>Every parcel leaves in double-walled board with corner protection; ask for crate packaging on anything over 30kg.</p>
143
+ <p>Photograph the box before opening and report damage within 48 hours; we replace or refund, never both.</p>
144
+ <p>Unopened stock returns within 30 days for a full refund; opened stock returns for store credit only.</p>
145
+ <p>Depots outside the primary freight lanes add one to three business days and a remote-area surcharge.</p>
146
+ <p>A single parcel tops out at 40kg; heavier loads split automatically at checkout.</p>
147
+ <p>Cross-border shipments carry the recipient's own duties and taxes, billed separately by the carrier.</p>
148
+ <p>Reach the shipping desk at shipping@example.com for anything this page does not answer.</p>
149
+ </div>
150
+ <DialogFooter>
151
+ <DialogClose asChild>
152
+ <Button>Done</Button>
153
+ </DialogClose>
154
+ </DialogFooter>
155
+ </DialogContent>
156
+ </Dialog>
157
+ )
158
+ }
159
+
160
+ <LongContentExample />
161
+ ```
162
+
163
+ ## Keyboard
164
+
165
+ | Keys | Result |
166
+ |---|---|
167
+ | `Tab` | Moves to the next control inside the dialog, looping from the last back to the first. |
168
+ | `Shift` `Tab` | Moves back, looping from the first to the last. |
169
+ | `Esc` | Closes it and sends focus back to whatever opened it. |
170
+
171
+ ## Do
172
+
173
+ - Put the actions in DialogFooter. It lays them out in a row, aligned right, with the gap between buttons already set.
174
+ - Write the body as plain elements inside DialogContent. It adds the padding and sets the body text (14px, meaning 14 pixels, in text.primary), so a bare paragraph already looks right.
175
+ - Write the title as the task: 'Edit depot', not 'Edit'.
176
+ - Put the safe action first in the footer.
177
+ - Open it from a button, a trigger or a shortcut. Focus goes back to that control when it closes.
178
+ - For long content with no field at the top, focus the panel itself when it opens (onOpenAutoFocus, tabIndex={-1}), not whatever button happens to come first.
179
+
180
+ ## Don't
181
+
182
+ - Don't open a dialog from a dialog. Finish or replace the first one.
183
+ - Don't open one from a dropdown menu item. The item is gone by the time the dialog closes, so focus has nowhere to go back to.
184
+ - Don't use one for a message that doesn't need a decision. That's a toast.
185
+ - Don't check an untouched field on blur (when focus leaves it) in a dialog's form, and don't show ErrorSummary before a submit. The first field has focus when the dialog opens, so pressing Cancel blurs it first. An error appearing at that moment pushes the footer down mid-click, and Cancel never closes the dialog.
186
+
187
+ ## Anatomy
188
+
189
+ - **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 dialog.
190
+ - **Surface**: surface.base, a strong hairline border and the corner crosshair. It's opaque, so every colour on it is measured against the page. Its motion is M7: it settles in with M1, and when it closes it shrinks slightly as it fades while the scrim fades behind it.
191
+ - **Title**: Required. An h2 (a second-level heading), and the dialog's accessible name, the name screen readers say for it.
192
+ - **Description**: Optional. The accessible description, which screen readers read after the name.
193
+ - **Body**: Anything: fields, text, a list.
194
+ - **Footer**: The actions, with the safe one first.
195
+ - **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,95 @@
1
+ # Divider
2
+
3
+ > A thin line, with an optional legend (a short text label). When it's only decoration, screen readers skip it. When it marks where a section starts, it's a real separator they announce.
4
+
5
+ - **Best for:** A thin line between groups of content, labelled when it starts a new section.
6
+ - **Not for:** Making space (use the spacing scale), or going between items that already have their own borders.
7
+ - Atom. Source: `src/components/atoms/divider/Divider.tsx`
8
+ - Rendered docs page: `/components/divider`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Divider } from 'src/components/atoms/divider/Divider'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Which way the line runs. |
21
+ | `label` | `string` | | Adds a legend and makes the divider more than decoration. |
22
+ | `className` | `string` | | Merged with the component's classes. When Tailwind utility classes clash, the later one wins. |
23
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the root element. |
24
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'role' \| 'children'>` | | Every native div prop except role and children. role is removed from the types rather than quietly overwritten when the page runs, because label already decides whether the divider is decorative or structural. children is removed because it behaved two different ways: with no label it rendered inside a decorative separator, and with one it disappeared. |
25
+
26
+ ## Examples
27
+
28
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
29
+
30
+ ### Plain rule
31
+
32
+ ```tsx
33
+ <div className="w-full max-w-sm">
34
+ <Divider />
35
+ </div>
36
+ ```
37
+
38
+ ### With a legend
39
+
40
+ ```tsx
41
+ <div className="w-full max-w-sm">
42
+ <Divider label="Section 02" />
43
+ </div>
44
+ ```
45
+
46
+ ### Vertical, between inline items
47
+
48
+ ```tsx
49
+ <span className="flex items-center gap-3 h-6">
50
+ <span className="text-sm">Kit list</span>
51
+ <Divider orientation="vertical" />
52
+ <span className="text-sm">Load-out</span>
53
+ </span>
54
+ ```
55
+
56
+ ### Drawing on
57
+
58
+ ```tsx
59
+ <div className="flex flex-col gap-4 w-full max-w-sm">
60
+ {/* The Button atom, not a bare <button>: a hand-rolled one here had no system focus
61
+ ring at all (the UA default, rgb(16, 16, 16) auto 1px) and no type, so it defaulted
62
+ to submit. */}
63
+ <Button variant="secondary" size="sm" wrapperClassName="w-fit" onClick={() => setReplayKey(k => k + 1)}>
64
+ Replay
65
+ </Button>
66
+ <Divider key={replayKey} className="anim-draw-on" />
67
+ <p className="text-xs text-[var(--color-text-muted)]">
68
+ This animation is switched off completely when someone has asked their device for
69
+ less motion (the prefers-reduced-motion: reduce setting).
70
+ </p>
71
+ </div>
72
+ ```
73
+
74
+ ## Keyboard
75
+
76
+ Not focusable. No keyboard behaviour of its own.
77
+
78
+ ## Do
79
+
80
+ - Leave it decorative when it's only there for visual rhythm.
81
+ - Label it when it really starts a new section, so screen readers announce the structure.
82
+ - Add anim-draw-on when a section animates in. The line drawing itself is motion token M2.
83
+
84
+ ## Don't
85
+
86
+ - Don't stack dividers to make space. That's what the spacing scale is for.
87
+ - Don't label every divider. If everything is a landmark, nothing is.
88
+ - Don't use a vertical divider between items that already have their own borders.
89
+ - Don't pass role. It's removed from the types so nothing can override it, and label already decides none vs separator.
90
+
91
+ ## Anatomy
92
+
93
+ - **Rule**: 1px (one pixel) in border.hairline.
94
+ - **Legend**: JetBrains Mono, 10px, muted. Optional.
95
+ - **role**: none when decorative, separator when labelled. role tells screen readers what an element is.
@@ -0,0 +1,220 @@
1
+ # DropdownMenu
2
+
3
+ > A short list of actions behind a button. It uses the same background as Select, so the system's two pop-ups look like one family. The highlighted item gets a fill, as it does everywhere else.
4
+
5
+ - **Best for:** A short list of actions, under about eight, behind a button.
6
+ - **Not for:** Picking a value for a form: use Select.
7
+ - Molecule. Source: `src/components/molecules/dropdown-menu/DropdownMenu.tsx`
8
+ - Rendered docs page: `/components/dropdown-menu`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from 'src/components/molecules/dropdown-menu/DropdownMenu'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `open (Root)` | `boolean` | | Whether the menu is open, when the page keeps track of it (controlled). |
21
+ | `defaultOpen (Root)` | `boolean` | | Whether it starts open, when the menu keeps track itself (uncontrolled). |
22
+ | `onOpenChange (Root)` | `(open: boolean) => void` | | Called when the menu opens or closes. |
23
+ | `modal (Root)` | `boolean` | `true` | While open, keyboard focus stays inside the menu and the page behind can't scroll. |
24
+ | `align (Content)` | `'start' \| 'center' \| 'end'` | `'center'` | Alignment against the trigger. |
25
+ | `side (Content)` | `'top' \| 'right' \| 'bottom' \| 'left'` | | Preferred side of the trigger. |
26
+ | `sideOffset (Content)` | `number` | `4` | Gap between trigger and content, in pixels. |
27
+ | `onSelect (Item)` | `(event: Event) => void` | | Called when the item is chosen. |
28
+ | `disabled (Item)` | `boolean` | `false` | The item can't be chosen, and typing letters (typeahead) skips it. |
29
+ | `tone (Item)` | `'default' \| 'hostile'` | `'default'` | Use hostile for a destructive action: hostile-coloured text at rest, the hostile fill with ink text when highlighted. |
30
+ | `icon (Item)` | `ReactNode` | | A small icon before the text, hidden from screen readers (aria-hidden). |
31
+ | `shortcut (Item)` | `string` | | Shown on the right and passed to screen readers as aria-keyshortcuts. It only displays the key: the menu never makes the key work. |
32
+ | `checked (Checkbox item)` | `boolean` | | Whether the tick shows. |
33
+ | `onCheckedChange (Checkbox item)` | `(checked: boolean) => void` | | Called when the checkbox item is toggled. |
34
+ | `value (RadioGroup)` | `string` | | The checked radio item. |
35
+ | `onValueChange (RadioGroup)` | `(value: string) => void` | | Called with the chosen radio item value. |
36
+ | `value (RadioItem)` | `string` | | This radio item's own value. |
37
+ | `icon (SubTrigger)` | `ReactNode` | | A small icon before the text, hidden from screen readers, as on Item. |
38
+ | `open / defaultOpen / onOpenChange (Sub)` | `boolean / boolean / (open: boolean) => void` | | Whether the submenu is open, tracked by the page (controlled) or by the submenu (uncontrolled). You'll usually leave these alone. |
39
+ | `alignOffset (SubContent)` | `number` | `-5` | Shifts the submenu so its first row lines up with its trigger. |
40
+ | `className` | `string` | | Extra CSS classes, merged with the part's own. When two Tailwind classes clash, the later one wins. Every part takes it. |
41
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the rendered element (the DOM node). On Content, Item, Checkbox item, Radio item, Sub trigger and Sub content. |
42
+
43
+ ## Examples
44
+
45
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
46
+
47
+ ### Actions
48
+
49
+ ```tsx
50
+ function ActionsExample() {
51
+ const [lastAction, setLastAction] = useState('No action yet.')
52
+ const [pinned, setPinned] = useState(false)
53
+
54
+ return (
55
+ <div className="flex flex-col items-start gap-3">
56
+ <DropdownMenu>
57
+ <DropdownMenuTrigger asChild>
58
+ <Button variant="secondary">Manifest actions</Button>
59
+ </DropdownMenuTrigger>
60
+ <DropdownMenuContent>
61
+ <DropdownMenuLabel>Manifest</DropdownMenuLabel>
62
+ <DropdownMenuItem shortcut="D" onSelect={() => setLastAction('Duplicated.')}>
63
+ Duplicate
64
+ </DropdownMenuItem>
65
+ <DropdownMenuCheckboxItem
66
+ checked={pinned}
67
+ onCheckedChange={checked => {
68
+ setPinned(checked)
69
+ setLastAction(checked ? 'Pinned to top.' : 'Unpinned.')
70
+ }}
71
+ >
72
+ Pin to top
73
+ </DropdownMenuCheckboxItem>
74
+ <DropdownMenuSeparator />
75
+ <DropdownMenuItem tone="hostile" onSelect={() => setLastAction('Archived.')}>
76
+ Archive
77
+ </DropdownMenuItem>
78
+ </DropdownMenuContent>
79
+ </DropdownMenu>
80
+ <p className={docsProse}>{lastAction}</p>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ <ActionsExample />
86
+ ```
87
+
88
+ ### With icons and shortcuts
89
+
90
+ ```tsx
91
+ function IconsAndShortcutsExample() {
92
+ return (
93
+ <DropdownMenu>
94
+ <DropdownMenuTrigger asChild>
95
+ <Button variant="secondary">Depot manifest</Button>
96
+ </DropdownMenuTrigger>
97
+ <DropdownMenuContent>
98
+ <DropdownMenuItem icon={<Copy />} shortcut="⌘C">
99
+ Copy
100
+ </DropdownMenuItem>
101
+ <DropdownMenuItem icon={<CopyPlus />} shortcut="⌘D">
102
+ Duplicate
103
+ </DropdownMenuItem>
104
+ <DropdownMenuItem icon={<Download />} shortcut="⌘E">
105
+ Export
106
+ </DropdownMenuItem>
107
+ </DropdownMenuContent>
108
+ </DropdownMenu>
109
+ )
110
+ }
111
+
112
+ <IconsAndShortcutsExample />
113
+ ```
114
+
115
+ ### Radio items and a submenu
116
+
117
+ ```tsx
118
+ function RadioAndSubmenuExample() {
119
+ const [sort, setSort] = useState('newest')
120
+ const [moved, setMoved] = useState('Not moved.')
121
+
122
+ return (
123
+ <div className="flex flex-col items-start gap-3">
124
+ <DropdownMenu>
125
+ <DropdownMenuTrigger asChild>
126
+ <Button variant="secondary">Replica options</Button>
127
+ </DropdownMenuTrigger>
128
+ <DropdownMenuContent>
129
+ <DropdownMenuLabel>Sort by</DropdownMenuLabel>
130
+ <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
131
+ <DropdownMenuRadioItem value="newest">Newest</DropdownMenuRadioItem>
132
+ <DropdownMenuRadioItem value="price">Price</DropdownMenuRadioItem>
133
+ <DropdownMenuRadioItem value="weight">Weight</DropdownMenuRadioItem>
134
+ </DropdownMenuRadioGroup>
135
+ <DropdownMenuSeparator />
136
+ <DropdownMenuSub>
137
+ <DropdownMenuSubTrigger icon={<FolderInput />}>Move to</DropdownMenuSubTrigger>
138
+ <DropdownMenuSubContent>
139
+ <DropdownMenuItem onSelect={() => setMoved('Moved to the armoury.')}>Armoury</DropdownMenuItem>
140
+ <DropdownMenuItem onSelect={() => setMoved('Moved to the workshop.')}>Workshop</DropdownMenuItem>
141
+ <DropdownMenuItem onSelect={() => setMoved('Moved to the depot.')}>Depot</DropdownMenuItem>
142
+ </DropdownMenuSubContent>
143
+ </DropdownMenuSub>
144
+ </DropdownMenuContent>
145
+ </DropdownMenu>
146
+ <p className={docsProse}>
147
+ Sorted by {sort}. {moved}
148
+ </p>
149
+ </div>
150
+ )
151
+ }
152
+
153
+ <RadioAndSubmenuExample />
154
+ ```
155
+
156
+ ### From an icon button
157
+
158
+ ```tsx
159
+ function IconButtonTriggerExample() {
160
+ return (
161
+ <DropdownMenu>
162
+ <DropdownMenuTrigger asChild>
163
+ <IconButton label="Depot actions" variant="secondary">
164
+ <MoreHorizontal aria-hidden="true" />
165
+ </IconButton>
166
+ </DropdownMenuTrigger>
167
+ <DropdownMenuContent align="end">
168
+ <DropdownMenuItem>View manifest</DropdownMenuItem>
169
+ <DropdownMenuItem>Reassign</DropdownMenuItem>
170
+ <DropdownMenuSeparator />
171
+ <DropdownMenuItem tone="hostile">Decommission</DropdownMenuItem>
172
+ </DropdownMenuContent>
173
+ </DropdownMenu>
174
+ )
175
+ }
176
+
177
+ <IconButtonTriggerExample />
178
+ ```
179
+
180
+ ## Keyboard
181
+
182
+ | Keys | Result |
183
+ |---|---|
184
+ | `Enter` `Space` `ArrowDown` | On the trigger: opens, highlighting the first item. |
185
+ | `ArrowUp` `ArrowDown` | Moves the highlight. |
186
+ | `Home` `End` | Jumps to the first or last item. |
187
+ | `Enter` `Space` | Runs the highlighted item. |
188
+ | `Escape` | Closes the menu and puts focus back on the trigger. |
189
+ | `ArrowRight` | On a sub trigger: opens the submenu, highlighting its first item. |
190
+ | `ArrowLeft` | In a submenu: closes it, and you're back on its trigger. |
191
+ | `A–Z` | Typeahead: jumps to the next item that starts with the letters you type. |
192
+
193
+ ## Do
194
+
195
+ - Keep menus under about eight items.
196
+ - Put the destructive item last, after a separator, with the hostile tone.
197
+ - Name an icon-only trigger for what the menu holds: 'Manifest actions', not 'More'.
198
+ - Use a radio group for a setting the menu shows the current value of: sort order, density.
199
+ - Name a sub trigger for what it holds: 'Move to', 'Export as'.
200
+
201
+ ## Don't
202
+
203
+ - Don't use a menu to pick a value for a form. That's what Select is for.
204
+ - Don't put a submenu inside a submenu. One level at most.
205
+ - Don't use radio items to run actions. A radio item sets a value, and the menu keeps showing it.
206
+ - Don't make every item hostile to add weight. Hostile means it destroys something.
207
+
208
+ ## Anatomy
209
+
210
+ - **Trigger**: A Button or IconButton, passed in through asChild. Radix (the library underneath) adds aria-haspopup and aria-expanded. These are ARIA (Accessible Rich Internet Applications) attributes that tell screen readers the button opens a menu and whether it's open.
211
+ - **Content**: Rendered in a portal, at the end of the page, so nothing clips it. Sunken surface, a strong thin border, crosshair corners. M7: it slides down out of the trigger to open and back up into it to close (the other way round when it opens above).
212
+ - **Item**: 36px (pixels) tall. When highlighted: the signal fill with ink-coloured text.
213
+ - **Hostile item**: For a destructive action. Hostile-coloured text at rest, and the hostile fill with ink text when highlighted.
214
+ - **Checkbox item**: Shows a tick when on. Screen readers get it as a menuitemcheckbox with aria-checked.
215
+ - **Label**: Uses text.secondary, so it reads as a heading over the items. It doesn't use text.label: on this background (surface.sunken) that token fails SC 1.4.3, the contrast rule in WCAG (the Web Content Accessibility Guidelines).
216
+ - **Separator**: A thin line.
217
+ - **Radio item**: One choice in a RadioGroup. Screen readers get it as a menuitemradio with aria-checked. Its mark is a small filled square, never a dot.
218
+ - **Sub trigger**: An item with a chevron that opens a submenu. While the highlight is inside the submenu, the sub trigger keeps a 1px strong line just inside its edge.
219
+ - **Sub content**: The submenu: the same surface and M7 slide as Content, aligned so its first row sits level with its trigger.
220
+ - **Shortcut**: Text on the right that screen readers skip (aria-hidden), plus aria-keyshortcuts on the item so they announce it once. It only displays the key: the page has to make the key work.
@@ -0,0 +1,143 @@
1
+ # EmptyState
2
+
3
+ > What a list, a table or a page region shows when it has nothing in it: an icon, a title that names what is empty, a sentence on why, and the action that fills it.
4
+
5
+ - **Best for:** A list, table or region with nothing in it, along with the action that fills it.
6
+ - **Not for:** A list that's still loading: use Skeleton or Spinner. A failed load: use Alert in the hostile tone.
7
+ - Molecule. Source: `src/components/molecules/empty-state/EmptyState.tsx`
8
+ - Rendered docs page: `/components/empty-state`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { EmptyState } from 'src/components/molecules/empty-state/EmptyState'
14
+ ```
15
+
16
+ ## Props
17
+
18
+ | Prop | Type | Default | Description |
19
+ |---|---|---|---|
20
+ | `title` | `string` | | Required. What is empty, said plainly: “No orders yet”. |
21
+ | `level` | `2 \| 3 \| 4` | | Required, with no default. The heading level the page's outline of headings needs at this point. |
22
+ | `description` | `ReactNode` | | One or two sentences: why it's empty, or what fills it. |
23
+ | `icon` | `ReactNode` | | A lucide icon element. The component sizes it and wraps it in a tile that screen readers skip (aria-hidden). |
24
+ | `actions` | `ReactNode` | | One or two Buttons. The component lays out the row and its gap. |
25
+ | `size` | `'sm' \| 'md'` | `'md'` | md for a page region. sm inside a table, a card or a panel. |
26
+ | `framed` | `boolean` | `true` | The dashed frame. Turn it off where the container already draws an edge. |
27
+ | `className` | `string` | | Extra CSS classes, merged onto the outer element. When two Tailwind classes clash, the later one wins. |
28
+ | `ref` | `Ref<HTMLDivElement>` | | Passed through to the outer element. |
29
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'title'>` | | Any other div attribute, put on the outer element. role="status" goes here when nothing else on the page tells screen readers that the list has emptied. |
30
+
31
+ ## Examples
32
+
33
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
34
+
35
+ ### A page region, after a filter
36
+
37
+ ```tsx
38
+ <EmptyState
39
+ level={4}
40
+ icon={<PackageSearch />}
41
+ title="No replicas match"
42
+ description="Nothing in stock matches all three filters. Remove one, or clear them and start again."
43
+ actions={
44
+ <>
45
+ <Button>Clear filters</Button>
46
+ <Button variant="secondary">Browse all replicas</Button>
47
+ </>
48
+ }
49
+ />
50
+ ```
51
+
52
+ ### First use, one action
53
+
54
+ ```tsx
55
+ <EmptyState
56
+ level={4}
57
+ icon={<Heart />}
58
+ title="Your wishlist is empty"
59
+ description="Save a replica from its page and it waits here, with its price and stock."
60
+ actions={<Button variant="secondary">Browse replicas</Button>}
61
+ />
62
+ ```
63
+
64
+ ### Small and unframed, inside a card
65
+
66
+ ```tsx
67
+ <Card as="div" className="max-w-md">
68
+ <CardBody>
69
+ <EmptyState
70
+ level={4}
71
+ size="sm"
72
+ framed={false}
73
+ icon={<StickyNote />}
74
+ title="No notes on this order"
75
+ description="Notes added at the counter show up here."
76
+ />
77
+ </CardBody>
78
+ </Card>
79
+ ```
80
+
81
+ ### Title only
82
+
83
+ ```tsx
84
+ <EmptyState level={4} size="sm" title="No returns yet" />
85
+ ```
86
+
87
+ ## Keyboard
88
+
89
+ | Keys | Result |
90
+ |---|---|
91
+ | `Tab` | Moves through the actions, if there are any. The empty state itself never takes keyboard focus. |
92
+
93
+ ## Do
94
+
95
+ - Name what is empty in the title (“No orders yet”), not the state (“Empty”).
96
+ - Say in the description what fills it, or why a filter emptied it.
97
+ - Offer the action that fixes it: “Clear filters” after a filter, “Browse replicas” on an empty wishlist.
98
+ - Use size="sm" and framed={false} inside a table, a card or a panel.
99
+
100
+ ## Don't
101
+
102
+ - Don’t use it for a list that’s still loading. Nothing has been found yet, so show a spinner or a skeleton.
103
+ - Don’t use it for an error. A failed load is an Alert in the hostile tone, with a retry.
104
+ - Don’t put more than two actions in it. A third belongs on the page, not in the empty region.
105
+ - Don’t add role="status" when a count on the page already announces the change. A screen reader user would hear it twice.
106
+
107
+ ## Common mistakes
108
+
109
+ Wrong:
110
+
111
+ ```tsx
112
+ <EmptyState title="No orders yet" />
113
+ ```
114
+
115
+ Right:
116
+
117
+ ```tsx
118
+ <EmptyState level={2} title="No orders yet" />
119
+ ```
120
+
121
+ The level is required, because only the page knows what the empty region is: a whole page (an h2, a level-2 heading, under the h1), a section (h3) or a card (h4).
122
+
123
+ Wrong:
124
+
125
+ ```tsx
126
+ <Card><EmptyState level={3} title="No notes" /></Card>
127
+ ```
128
+
129
+ Right:
130
+
131
+ ```tsx
132
+ <Card><EmptyState level={3} size="sm" framed={false} title="No notes" /></Card>
133
+ ```
134
+
135
+ The card already draws an edge, so a dashed frame inside it makes a second one. And the md (medium) padding is sized for a page region.
136
+
137
+ ## Anatomy
138
+
139
+ - **Frame**: A dashed edge in border.hairline-strong. It's the one border in the system that reads as an empty slot instead of a surface. Optional, through framed.
140
+ - **Icon tile**: Optional. A square on surface.sunken with a thin edge, holding a lucide icon in text.secondary. Screen readers skip it (aria-hidden), because the title already says what the icon would.
141
+ - **Title**: A heading element, h2 to h4. Oxanium, semibold, 18px (pixels) at md size and 16px at sm (small), in text.primary.
142
+ - **Description**: Optional. 14px body text in text.secondary, with lines no longer than 48 characters.
143
+ - **Actions**: Optional. One or two Buttons, centred, in a row that wraps onto a new line with a 12px gap.