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,140 @@
1
+ # QuantityStepper
2
+
3
+ > A whole number between a lowest and a highest value, with a button on each side. It follows the spinbutton pattern (the standard recipe for a number you nudge up and down), so the keyboard can do everything from inside the field.
4
+
5
+ - **Best for:** A whole number between real limits, such as a quantity in a cart.
6
+ - **Not for:** A value that isn't a whole number: use Input. A range of hundreds: use Slider or Input.
7
+ - Molecule. Source: `src/components/molecules/quantity-stepper/QuantityStepper.tsx`
8
+ - Rendered docs page: `/components/quantity-stepper`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { QuantityStepper, clampQuantity, gridMax } from 'src/components/molecules/quantity-stepper/QuantityStepper'
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 field and both buttons ("Decrease Speed loader quantity"). |
23
+ | `value` | `number` | | Controlled value (your page holds it). |
24
+ | `defaultValue` | `number` | `min` | Starting value when the component holds the value itself (uncontrolled). |
25
+ | `onValueChange` | `(value: number) => void` | | Fires with the final value, pulled inside the limits: after a button click, an arrow key, Home/End, or when typing is finished by leaving the field or pressing Enter. |
26
+ | `min` | `number` | `0` | Lowest allowed value. |
27
+ | `max` | `number` | `99` | Highest allowed value. |
28
+ | `step` | `number` | `1` | The size of each step the value snaps to, counted from min. |
29
+ | `disabled` | `boolean` | `false` | Disables the field and both buttons. |
30
+ | `inputSize` | `'sm' \| 'md'` | `'md'` | Size of the field and buttons. |
31
+ | `className` | `string` | | Combined with the root classes. Where Tailwind utility classes clash, the later one wins. |
32
+ | `ref` | `Ref<HTMLInputElement>` | | Passed through to the field. |
33
+ | `id` | `string` | | The field's id, so a FormField's label and error text can point at it. FormField sets it. |
34
+ | `aria-describedby` | `string` | | The ids of the help and error text, set on the field so screen readers read them out. FormField sets it. |
35
+ | `invalid` | `boolean` | | Marks the field as invalid: aria-invalid and the invalid border. FormField sets it when there is an error. |
36
+ | `required` | `boolean` | | Marks the field as required. FormField sets it. |
37
+
38
+ ## Types and helpers
39
+
40
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
41
+
42
+ ```ts
43
+ /** The step grid's own last rung at or below `max` -- the real upper bound the control can reach.
44
+ * `max` itself may not be a step away from `min` (min 0, max 99, step 5: the grid is 0, 5, ...,
45
+ * 95, and 99 is never a rung on it), so this is what `aria-valuemax`, the Increase button's
46
+ * disabled state, the focus-on-bound check and End all need instead of `max` directly -- a control
47
+ * that can never reach the value it announces as its maximum is worse than one that announces a
48
+ * lower, reachable one. */
49
+ export function gridMax(min: number, max: number, step = 1): number
50
+ ```
51
+
52
+ ```ts
53
+ /**
54
+ * Pure. Parses, snaps to the step grid measured from min, then clamps into `[min, gridMax(min,
55
+ * max, step)]`. Unparseable input returns `min`.
56
+ *
57
+ * `min > max` is the caller's error, not this function's to reject: the formula has no branch that
58
+ * assumes `min <= max`, so it still returns a number rather than throwing -- just not a meaningful
59
+ * one for an inverted range. Callers are expected to pass a sane range.
60
+ */
61
+ export function clampQuantity(raw: string | number, min: number, max: number, step = 1): number
62
+ ```
63
+
64
+ ## Examples
65
+
66
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
67
+
68
+ ### Default
69
+
70
+ ```tsx
71
+ <QuantityStepper label="Speed loader quantity" defaultValue={1} />
72
+ ```
73
+
74
+ ### Bounded: with a lowest and a highest allowed number
75
+
76
+ ```tsx
77
+ <div className="flex flex-col gap-2">
78
+ <QuantityStepper label="Bump helmet quantity" defaultValue={1} min={1} max={4} />
79
+ <span className="font-mono text-xs text-[var(--color-text-secondary)]">Only four left</span>
80
+ </div>
81
+ ```
82
+
83
+ ### Stepping by five
84
+
85
+ ```tsx
86
+ <QuantityStepper label="BB bottle quantity" defaultValue={5} min={0} max={50} step={5} />
87
+ ```
88
+
89
+ ### In a cart line
90
+
91
+ ```tsx
92
+ function CartLine() {
93
+ const price = 38
94
+ const [qty, setQty] = useState(2)
95
+ return (
96
+ <div className="flex items-center gap-4 w-full">
97
+ <span className="flex-1 font-body text-sm text-[var(--color-text-primary)]">Speed loader</span>
98
+ <QuantityStepper
99
+ label="Speed loader quantity"
100
+ defaultValue={2}
101
+ min={1}
102
+ max={9}
103
+ onValueChange={setQty}
104
+ />
105
+ <span className="w-16 text-right font-display font-semibold tabular-nums text-[var(--color-text-primary)]">
106
+ €{price * qty}
107
+ </span>
108
+ </div>
109
+ )
110
+ }
111
+
112
+ <CartLine />
113
+ ```
114
+
115
+ ## Keyboard
116
+
117
+ | Keys | Result |
118
+ |---|---|
119
+ | `↑` `↓` | One step. |
120
+ | `Page Up` `Page Down` | Ten steps. |
121
+ | `Home` `End` | Jumps to the minimum or maximum. |
122
+ | `Enter` | Confirms what you typed. |
123
+ | `Tab` | Leaves the field and confirms what you typed. Tab doesn't stop on the buttons. |
124
+
125
+ ## Do
126
+
127
+ - Set a real maximum, like the stock level or an order limit, not an arbitrary 99.
128
+ - Name the field after the item: 'Speed loader quantity'.
129
+ - Show the line total beside it, updating as the value changes.
130
+
131
+ ## Don't
132
+
133
+ - Don't use it for values that aren't whole numbers; that's an Input.
134
+ - Don't use it for a range of hundreds; that's a Slider or an Input.
135
+ - Don't pull the value back inside the limits while someone's still typing.
136
+
137
+ ## Anatomy
138
+
139
+ - **Field**: Has role=spinbutton, plus its value and limits, so screen readers know what it is. It's a text field that brings up a number keypad on phones, not the browser's own number input.
140
+ - **Decrease / Increase**: Secondary IconButtons named after the field. As the pattern says, Tab skips them. They're disabled at the lowest and highest value.
@@ -0,0 +1,177 @@
1
+ # Radio
2
+
3
+ > Pick one choice from a set. The whole group is a single tab stop, and the arrow keys move inside it. That’s how native radio buttons behave, and it’s most of the reason this is built on a Radix primitive (Radix is a library of accessible building blocks) instead of three styled inputs.
4
+
5
+ - **Best for:** One choice from a short set, up to about seven, all shown at once.
6
+ - **Not for:** A yes/no question: use Checkbox or Switch. A longer list: use Select.
7
+ - Atom. Source: `src/components/atoms/radio/Radio.tsx`
8
+ - Rendered docs page: `/components/radio`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Radio, RadioGroup } from 'src/components/atoms/radio/Radio'
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
+ | `value (RadioGroup)` | `string` | | The selected value, when you control it from outside. |
23
+ | `defaultValue (RadioGroup)` | `string` | | The starting value, when the group manages it itself (uncontrolled). |
24
+ | `onValueChange (RadioGroup)` | `(value: string) => void` | | Fires when the selection changes. |
25
+ | `name (RadioGroup)` | `string` | | The native form field name. |
26
+ | `required (RadioGroup)` | `boolean` | | Uses the browser’s native form validation. |
27
+ | `disabled (RadioGroup)` | `boolean` | | Disables every option in the group. |
28
+ | `orientation (RadioGroup)` | `'horizontal' \| 'vertical'` | | Sets which arrow keys move between options. The visual layout is still up to className. |
29
+ | `aria-label / aria-labelledby (RadioGroup)` | `string` | | One of these is required. They’re ARIA attributes (from Accessible Rich Internet Applications) that give the group a name screen readers announce. A radiogroup with no name is a bug. |
30
+ | `value (Radio)` | `string` | | Required. This option’s value. |
31
+ | `disabled (Radio)` | `boolean` | | Takes just this option out of selection. |
32
+ | `id (Radio)` | `string` | | Pair it with a real label using htmlFor. |
33
+ | `invalid (Radio)` | `boolean` | `false` | Sets aria-invalid, which tells assistive tech the field has an error, and shows the invalid border. |
34
+ | `className` | `string` | | Merged with the control's classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
35
+ | `ref` | `Ref<HTMLDivElement> (RadioGroup) / Ref<HTMLButtonElement> (Radio)` | | Passed through to the Radix root element. |
36
+ | `...rest` | `(Radio) Omit<RadioGroup.Item props, 'aria-invalid'>` | | Every Radix RadioGroup.Item prop except aria-invalid, spread onto the item. aria-invalid is typed away instead of being quietly overridden at runtime, because invalid is the only source of both the attribute and the border. RadioGroup itself excludes nothing. It computes no value of its own that a rest prop could defeat. |
37
+
38
+ ## Examples
39
+
40
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
41
+
42
+ ### A group
43
+
44
+ ```tsx
45
+ const SPEEDS = ['standard', 'priority', 'overnight'] as const
46
+
47
+ function LiveExample() {
48
+ const [value, setValue] = useState<string>('standard')
49
+ return (
50
+ <RadioGroup aria-label="Dispatch speed" value={value} onValueChange={setValue}>
51
+ {SPEEDS.map(speed => (
52
+ <div key={speed} className="flex items-center gap-2">
53
+ <Radio value={speed} id={`dispatch-${speed}`} />
54
+ <label htmlFor={`dispatch-${speed}`} className="capitalize">
55
+ {speed}
56
+ </label>
57
+ </div>
58
+ ))}
59
+ </RadioGroup>
60
+ )
61
+ }
62
+
63
+ <LiveExample />
64
+ ```
65
+
66
+ ### With a default
67
+
68
+ ```tsx
69
+ const SPEEDS = ['standard', 'priority', 'overnight'] as const
70
+
71
+ <RadioGroup aria-label="Dispatch speed, defaulted" defaultValue="priority">
72
+ {SPEEDS.map(speed => (
73
+ <div key={speed} className="flex items-center gap-2">
74
+ <Radio value={speed} id={`default-${speed}`} />
75
+ <label htmlFor={`default-${speed}`} className="capitalize">
76
+ {speed}
77
+ </label>
78
+ </div>
79
+ ))}
80
+ </RadioGroup>
81
+ ```
82
+
83
+ ### Horizontal
84
+
85
+ ```tsx
86
+ const SPEEDS = ['standard', 'priority', 'overnight'] as const
87
+
88
+ <RadioGroup aria-label="Dispatch speed, horizontal" orientation="horizontal" defaultValue="standard" className="flex-row flex-wrap gap-4">
89
+ {SPEEDS.map(speed => (
90
+ <div key={speed} className="flex items-center gap-2">
91
+ <Radio value={speed} id={`horizontal-${speed}`} />
92
+ <label htmlFor={`horizontal-${speed}`} className="capitalize">
93
+ {speed}
94
+ </label>
95
+ </div>
96
+ ))}
97
+ </RadioGroup>
98
+ ```
99
+
100
+ ### Disabled option
101
+
102
+ ```tsx
103
+ const SPEEDS = ['standard', 'priority', 'overnight'] as const
104
+
105
+ <RadioGroup aria-label="Dispatch speed, one option disabled" defaultValue="standard">
106
+ {SPEEDS.map(speed => (
107
+ <div key={speed} className="flex items-center gap-2">
108
+ <Radio value={speed} id={`disabled-${speed}`} disabled={speed === 'overnight'} />
109
+ <label htmlFor={`disabled-${speed}`} className="capitalize">
110
+ {speed}
111
+ </label>
112
+ </div>
113
+ ))}
114
+ </RadioGroup>
115
+ ```
116
+
117
+ ## Keyboard
118
+
119
+ | Keys | Result |
120
+ |---|---|
121
+ | `Tab` | Enters the group at the selected option, or at the first one if nothing’s selected. |
122
+ | `↓` `→` | Selects the next option. |
123
+ | `↑` `←` | Selects the previous option. |
124
+ | `Space` | Selects the focused option. |
125
+
126
+ ## Do
127
+
128
+ - Name the group: in a form, a fieldset legend that the RadioGroup points at with aria-labelledby; elsewhere, aria-label.
129
+ - Offer a default when one option really is the norm.
130
+ - Keep the set short. Past about seven options, use a Select.
131
+
132
+ ## Don't
133
+
134
+ - Don't use radios for a yes/no. That's a Checkbox or a Switch.
135
+ - Don't put a radio outside a RadioGroup. Both the roving focus and the value come from the group.
136
+ - Don't make choosing an option perform the action. Put a button after the group.
137
+
138
+ ## Common mistakes
139
+
140
+ Wrong:
141
+
142
+ ```tsx
143
+ <FormField label="Difficulty" error={error}>
144
+ <RadioGroup>
145
+ <Radio value="easy" />
146
+ </RadioGroup>
147
+ </FormField>
148
+ ```
149
+
150
+ Right:
151
+
152
+ ```tsx
153
+ <fieldset aria-describedby={error ? 'difficulty-error' : undefined}>
154
+ <legend id="difficulty-label" className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-primary)]">
155
+ Difficulty
156
+ </legend>
157
+ <RadioGroup aria-labelledby="difficulty-label" aria-required="true" value={value} onValueChange={setValue} className="mt-2">
158
+ {['easy', 'moderate', 'hard'].map(option => (
159
+ <div key={option} className="flex items-center gap-2">
160
+ <Radio id={`difficulty-${option}`} value={option} invalid={Boolean(error)} />
161
+ <label htmlFor={`difficulty-${option}`} className="font-body text-sm">{option}</label>
162
+ </div>
163
+ ))}
164
+ </RadioGroup>
165
+ {error ? (
166
+ <p id="difficulty-error" className="mt-1.5 font-body text-sm text-[var(--color-accent-hostile-text)]">{error}</p>
167
+ ) : null}
168
+ </fieldset>
169
+ ```
170
+
171
+ FormField clones one control and gives it an id and a label; a group of radios has neither. The fieldset and legend name the group, the label styles match FormField, and the error is linked through aria-describedby. Give ErrorSummary the id of the first Radio so its link lands in the group.
172
+
173
+ ## Anatomy
174
+
175
+ - **RadioGroup**: Has role=radiogroup, which tells assistive tech (screen readers and similar tools) this is a set of radios. It handles roving focus, where only one option is reachable by Tab and the arrows move between the rest, and it owns the value.
176
+ - **Radio**: Has role=radio. A 20px (pixel) square inside a 24px hit area, the region that responds to a click or tap.
177
+ - **Indicator**: A filled 10px square, not a dot. The system’s shape language has no circles.
@@ -0,0 +1,106 @@
1
+ # Rating
2
+
3
+ > A score out of five, either shown or collected. The display is one image that a screen reader reads as one sentence. The input is a radio group, because picking a rating means picking one value.
4
+
5
+ - **Best for:** Showing or collecting a score out of five.
6
+ - **Not for:** Anything that isn't a rating, such as a difficulty grade.
7
+ - Molecule. Source: `src/components/molecules/rating/Rating.tsx`
8
+ - Rendered docs page: `/components/rating`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Rating, RatingInput } from 'src/components/molecules/rating/Rating'
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
+ | `value (Rating)` | `number` | | From 0 up to max, rounded to the nearest half for both the drawing and what screen readers say. |
23
+ | `max (Rating)` | `number` | `5` | Number of stars. |
24
+ | `label (Rating)` | `string` | | What is being rated, read out as "<label> rated 4.5 out of 5". |
25
+ | `showValue (Rating)` | `boolean` | `false` | Prints the rounded value beside the stars, aria-hidden. |
26
+ | `size (Rating)` | `'sm' \| 'md'` | `'md'` | Star size. |
27
+ | `className (Rating)` | `string` | | Combined with the root classes. Where Tailwind utility classes clash, the later one wins. |
28
+ | `ref (Rating)` | `Ref<HTMLSpanElement>` | | Passed through to the root span. |
29
+ | `label (RatingInput)` | `string` | | Required. The radiogroup's name, for example "Rate the Prism holo sight". |
30
+ | `value (RatingInput)` | `number` | | The selected value, when your page holds it (controlled). |
31
+ | `defaultValue (RatingInput)` | `number` | | Starting value when the component holds it itself (uncontrolled). |
32
+ | `onValueChange (RatingInput)` | `(value: number) => void` | | Fires when a star is picked by click or arrow key. |
33
+ | `max (RatingInput)` | `number` | `5` | Number of stars. |
34
+ | `disabled (RatingInput)` | `boolean` | | Disables every star. |
35
+ | `className (RatingInput)` | `string` | | Combined with the root classes. |
36
+ | `ref (RatingInput)` | `Ref<HTMLDivElement>` | | Passed through to the Radix radiogroup root. It's built straight on that, not on the Radio atom: see the anatomy row above. |
37
+
38
+ ## Examples
39
+
40
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
41
+
42
+ ### Display
43
+
44
+ ```tsx
45
+ <div className="flex flex-col gap-3">
46
+ <Rating value={4.5} label="Prism holo sight" showValue />
47
+ <Rating value={3} label="Bump helmet" showValue />
48
+ <Rating value={5} label="Field goggles" showValue />
49
+ <Rating value={0.5} label="Dummy radio" showValue />
50
+ </div>
51
+ ```
52
+
53
+ ### Small, beside a price
54
+
55
+ ```tsx
56
+ <div className="flex items-center gap-2">
57
+ <span className="font-display font-semibold">€128</span>
58
+ <Rating value={4} size="sm" label="Prism holo sight" />
59
+ </div>
60
+ ```
61
+
62
+ ### Input
63
+
64
+ ```tsx
65
+ function LiveInput() {
66
+ const [value, setValue] = useState(4)
67
+ return (
68
+ <div className="flex items-center gap-3">
69
+ <RatingInput label="Rate the Prism holo sight" value={value} onValueChange={setValue} />
70
+ <span className="font-mono text-xs text-[var(--color-text-secondary)]">
71
+ You rated it {value} {value === 1 ? 'star' : 'stars'}
72
+ </span>
73
+ </div>
74
+ )
75
+ }
76
+
77
+ <LiveInput />
78
+ ```
79
+
80
+ ## Keyboard
81
+
82
+ | Keys | Result |
83
+ |---|---|
84
+ | `Tab` | Moves into the group, onto the selected star, or the first one. |
85
+ | `←` `↑` `→` `↓` | One star less (Left or Up) or more (Right or Down), selecting as it goes. It wraps round: past the last star goes to the first, and past the first goes to the last. |
86
+ | `Space` | Selects the focused star. |
87
+
88
+ ## Do
89
+
90
+ - Name what is rated in the display's label when the surrounding page doesn't.
91
+ - Show the number of reviews next to the stars: "4.5 (212)".
92
+ - Label an input with the action: "Rate the Prism holo sight".
93
+
94
+ ## Don't
95
+
96
+ - Don't use more than five stars.
97
+ - Don't let hovering over a star select anything.
98
+ - Don't use stars for anything that isn't a rating, such as a difficulty grade.
99
+
100
+ ## Anatomy
101
+
102
+ - **Rating**: Has role=img and the name "4.5 out of 5", so a screen reader treats it as one picture. The stars are aria-hidden (screen readers skip them).
103
+ - **Star**: Full, half or empty. Filled ones use the signal colour; empty ones are a border.selection outline.
104
+ - **Value**: An optional printed number. It's aria-hidden because the name already says it.
105
+ - **RatingInput**: A radiogroup named by label, built straight on Radix RadioGroup (Radix is the library of unstyled, accessible building blocks this system uses) instead of the Radio atom. Radio draws a 20px (pixel) square box with a filled inner square to show it's selected, and a star can't be drawn inside a square without either cropping it or leaving the box empty around it.
106
+ - **Star option**: A 28px radio named "3 stars".
@@ -0,0 +1,95 @@
1
+ # ScoreRing
2
+
3
+ > One 0 to 100 score shown as a ring with the number in its centre, split into bands the way Lighthouse (Google’s website audit tool) bands its scores. It’s a meter, so a screen reader, the software that reads the page aloud, hears both the score and its band.
4
+
5
+ - **Best for:** A 0 to 100 score with good and bad bands, like an audit, a health check or a quality score. Several side by side read as one report.
6
+ - **Not for:** Progress through a task: use Progress. A number with a change over time: use Stat. Parts of a whole: use a donut ChartPanel.
7
+ - Atom. Source: `src/components/atoms/score-ring/ScoreRing.tsx`
8
+ - Rendered docs page: `/components/score-ring`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { ScoreRing, scoreBand } from 'src/components/atoms/score-ring/ScoreRing'
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. What’s being scored. It’s both the accessible name and the caption. |
23
+ | `value` | `number` | | Required. 0 to 100, clamped and rounded to a whole number. |
24
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | The ring is 56, 96 or 128px (pixels) wide. |
25
+ | `className` | `string` | | Merged with the meter’s classes. Later Tailwind utilities (Tailwind is the CSS framework the system uses) win. |
26
+ | `ref` | `Ref<HTMLDivElement>` | | Points to the meter. |
27
+ | `...rest` | `Omit<HTMLAttributes<HTMLDivElement>, 'children'>` | | Every native div prop except children, spread onto the meter. The ring draws its own content. |
28
+ | `scoreBand()` | `(value: number) => 'hostile' \| 'warning' \| 'ok'` | | The banding the ring uses. It’s exported so a table or a sentence beside the ring agrees with it. |
29
+
30
+ ## Types and helpers
31
+
32
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
33
+
34
+ ```ts
35
+ export type ScoreBand = 'hostile' | 'warning' | 'ok'
36
+ ```
37
+
38
+ ```ts
39
+ /** Lighthouse's own bands, and its own words for them: 0-49 poor, 50-89 needs improvement, 90-100
40
+ * good. Exported so a table or a summary sentence beside the rings can band a score the same way. */
41
+ export function scoreBand(value: number): ScoreBand
42
+ ```
43
+
44
+ ## Examples
45
+
46
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
47
+
48
+ ### A report like Lighthouse (Google's website audit tool): four scores in a row
49
+
50
+ ```tsx
51
+ <ScoreRing label="Performance" value={42} />
52
+ <ScoreRing label="Accessibility" value={98} />
53
+ <ScoreRing label="Best practices" value={77} />
54
+ <ScoreRing label="SEO" value={100} />
55
+ ```
56
+
57
+ ### Sizes: sm, md and lg
58
+
59
+ ```tsx
60
+ <ScoreRing label="Stock health" value={64} size="sm" />
61
+ <ScoreRing label="Stock health" value={64} />
62
+ <ScoreRing label="Stock health" value={64} size="lg" />
63
+ ```
64
+
65
+ ### The band boundaries: 49, 50, 89 and 90
66
+
67
+ ```tsx
68
+ <ScoreRing label="Depot A" value={49} size="sm" />
69
+ <ScoreRing label="Depot B" value={50} size="sm" />
70
+ <ScoreRing label="Depot C" value={89} size="sm" />
71
+ <ScoreRing label="Depot D" value={90} size="sm" />
72
+ ```
73
+
74
+ ## Keyboard
75
+
76
+ Not focusable. No keyboard behaviour of its own.
77
+
78
+ ## Do
79
+
80
+ - Put several rings in a row for one report, like Lighthouse’s four scores.
81
+ - Name what’s being scored in a word or two.
82
+ - State the weakest score in the sentence beside the rings.
83
+
84
+ ## Don't
85
+
86
+ - Don't use it for a value that isn't out of 100. Rescale it first, or use Stat.
87
+ - Don't colour the number. The arc carries the band, and the number stays text.primary.
88
+
89
+ ## Anatomy
90
+
91
+ - **Ring**: A hairline track plus an arc as long as the score, with square (butt) ends.
92
+ - **Arc colour**: Set by band: hostile below 50, warning from 50 to 89, ok from 90. Colour is never the only signal, because the number is always printed.
93
+ - **Ticks**: Three marks outside the ring at 0, 50 and 90. They mark the start and the two band boundaries.
94
+ - **Number**: The score in display figures, centred in the ring.
95
+ - **Label**: Mono (a monospaced font), uppercase, under the ring. It’s also the meter’s accessible name, the name assistive tech announces for it.
@@ -0,0 +1,116 @@
1
+ # Search
2
+
3
+ > A search field inside a search landmark (a region of the page that screen readers can jump straight to). Enter submits, Escape clears, and a one-key shortcut can move focus to it from anywhere on the page.
4
+
5
+ - **Best for:** Searching the content of the page that shows the results, inside a search landmark.
6
+ - **Not for:** Suggestions that appear as you type, or jumping to an action: use CommandPalette.
7
+ - Molecule. Source: `src/components/molecules/search/Search.tsx`
8
+ - Rendered docs page: `/components/search`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Search } from 'src/components/molecules/search/Search'
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. The name screen readers use for the field, and for the search landmark. |
23
+ | `value` | `string` | | The value, when your page holds it (controlled). |
24
+ | `defaultValue` | `string` | `''` | Starting value when the component holds it itself (uncontrolled). |
25
+ | `onValueChange` | `(value: string) => void` | | Called whenever the value changes, clearing included. |
26
+ | `onSearch` | `(query: string) => void` | | Called on Enter with the value, spaces trimmed from both ends. It isn't called for an empty query. |
27
+ | `placeholder` | `string` | `'Search'` | |
28
+ | `shortcut` | `string` | | One printable key, for example "/". It moves focus to the field from anywhere on the page, except while someone is typing in another text field. While the field is empty, it shows as a Kbd hint. |
29
+ | `inputSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | |
30
+ | `className` | `string` | | Combined with the root classes. Where Tailwind utility classes clash, the later one wins. |
31
+ | `ref` | `Ref<HTMLInputElement>` | | Passed through to the input element. |
32
+
33
+ ## Examples
34
+
35
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
36
+
37
+ ### Default
38
+
39
+ ```tsx
40
+ <div className="max-w-xs w-full">
41
+ <Search label="Search the catalogue" />
42
+ </div>
43
+ ```
44
+
45
+ ### With a shortcut
46
+
47
+ ```tsx
48
+ <div className="max-w-xs w-full">
49
+ <Search label="Search the manifest" shortcut="/" />
50
+ <p className="font-body text-xs text-[var(--color-text-secondary)] mt-3">
51
+ Press / anywhere on this page to focus it.
52
+ </p>
53
+ </div>
54
+ ```
55
+
56
+ ### Submitting
57
+
58
+ ```tsx
59
+ function SubmittingExample() {
60
+ const [result, setResult] = useState<string | null>(null)
61
+
62
+ return (
63
+ <div className="max-w-xs w-full">
64
+ <Search label="Search the depot list" onSearch={query => setResult(query)} />
65
+ <p className="font-body text-xs text-[var(--color-text-secondary)] mt-3">
66
+ {result ? `Searched for: ${result}` : 'Press Enter to search.'}
67
+ </p>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ <SubmittingExample />
73
+ ```
74
+
75
+ ### Sizes
76
+
77
+ ```tsx
78
+ <div className="max-w-xs w-full">
79
+ <Search label="Search, small" placeholder="Small" inputSize="sm" />
80
+ </div>
81
+ <div className="max-w-xs w-full">
82
+ <Search label="Search, medium" placeholder="Medium" inputSize="md" />
83
+ </div>
84
+ <div className="max-w-xs w-full">
85
+ <Search label="Search, large" placeholder="Large" inputSize="lg" />
86
+ </div>
87
+ ```
88
+
89
+ ## Keyboard
90
+
91
+ | Keys | Result |
92
+ |---|---|
93
+ | `Enter` | Submits the query, spaces trimmed. |
94
+ | `Escape` | Clears the field. |
95
+ | `/` | Moves focus to the field, unless another text field has focus. It works for whichever key the shortcut prop names. |
96
+ | `Tab` | Moves between the field and the clear button. |
97
+
98
+ ## Do
99
+
100
+ - Name what's being searched: 'Search the catalogue', not 'Search'.
101
+ - Pick a shortcut that isn't a letter people type in forms; '/' is the usual one.
102
+ - Keep results on the page that owns the search, not in a popup under the field.
103
+
104
+ ## Don't
105
+
106
+ - Don't add suggestions that appear as you type. That's the command palette's job, and it has a listbox built for it.
107
+ - Don't search on every keystroke without saying so. Results that shift while someone types are hard to follow with a screen reader.
108
+ - Don't put two search landmarks on one page without giving them different labels.
109
+
110
+ ## Anatomy
111
+
112
+ - **Landmark**: A form with role=search, named by label, so people using a screen reader can jump to it.
113
+ - **Icon**: Decoration only. It's aria-hidden, so screen readers skip it.
114
+ - **Field**: An Input of type search. The browser's own clear button is hidden, because you can't reach it with the keyboard.
115
+ - **Clear button**: Only shows while there's text. It puts focus back in the field.
116
+ - **Shortcut hint**: A Kbd (the keyboard-key badge), aria-hidden, and only shown while the field is empty. Screen readers get the shortcut from aria-keyshortcuts on the field instead.