division-ds 0.2.0 → 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 (89) hide show
  1. package/AGENTS.md +9 -3
  2. package/division-ds-0.2.1.tgz +0 -0
  3. package/package.json +1 -1
  4. package/public/docs/components/accordion.md +2 -0
  5. package/public/docs/components/ai-label.md +2 -0
  6. package/public/docs/components/alert-dialog.md +2 -0
  7. package/public/docs/components/alert.md +2 -0
  8. package/public/docs/components/approval-request.md +2 -0
  9. package/public/docs/components/auth-form.md +2 -0
  10. package/public/docs/components/avatar-group.md +2 -0
  11. package/public/docs/components/avatar.md +2 -0
  12. package/public/docs/components/badge.md +2 -0
  13. package/public/docs/components/breadcrumbs.md +2 -0
  14. package/public/docs/components/button.md +2 -0
  15. package/public/docs/components/card.md +2 -0
  16. package/public/docs/components/carousel.md +2 -0
  17. package/public/docs/components/cart.md +2 -0
  18. package/public/docs/components/chart-panel.md +2 -0
  19. package/public/docs/components/chat-message.md +3 -0
  20. package/public/docs/components/chat-thread.md +2 -0
  21. package/public/docs/components/checkbox.md +2 -0
  22. package/public/docs/components/checkout-form.md +2 -0
  23. package/public/docs/components/command-palette.md +2 -0
  24. package/public/docs/components/data-table.md +2 -0
  25. package/public/docs/components/description-list.md +2 -0
  26. package/public/docs/components/dialog.md +2 -0
  27. package/public/docs/components/divider.md +2 -0
  28. package/public/docs/components/dropdown-menu.md +2 -0
  29. package/public/docs/components/empty-state.md +2 -0
  30. package/public/docs/components/footer.md +2 -0
  31. package/public/docs/components/form-field.md +4 -1
  32. package/public/docs/components/icon-button.md +2 -0
  33. package/public/docs/components/input.md +2 -0
  34. package/public/docs/components/kanban-board.md +2 -0
  35. package/public/docs/components/kbd.md +2 -0
  36. package/public/docs/components/link.md +2 -0
  37. package/public/docs/components/list-item.md +3 -0
  38. package/public/docs/components/loading-animation.md +2 -0
  39. package/public/docs/components/multi-select.md +2 -0
  40. package/public/docs/components/navbar.md +2 -0
  41. package/public/docs/components/newsletter-form.md +2 -0
  42. package/public/docs/components/page-header.md +2 -0
  43. package/public/docs/components/pagination.md +2 -0
  44. package/public/docs/components/product-card.md +3 -0
  45. package/public/docs/components/progress.md +3 -1
  46. package/public/docs/components/prompt-input.md +2 -0
  47. package/public/docs/components/quantity-stepper.md +2 -0
  48. package/public/docs/components/radio.md +38 -1
  49. package/public/docs/components/rating.md +2 -0
  50. package/public/docs/components/score-ring.md +2 -0
  51. package/public/docs/components/search.md +2 -0
  52. package/public/docs/components/section-heading.md +2 -0
  53. package/public/docs/components/segmented-control.md +2 -0
  54. package/public/docs/components/select.md +3 -1
  55. package/public/docs/components/sheet.md +2 -0
  56. package/public/docs/components/sidebar.md +2 -0
  57. package/public/docs/components/skeleton.md +2 -0
  58. package/public/docs/components/slider.md +3 -1
  59. package/public/docs/components/sparkline.md +2 -0
  60. package/public/docs/components/spinner.md +4 -2
  61. package/public/docs/components/stat.md +2 -0
  62. package/public/docs/components/status-tracker.md +2 -0
  63. package/public/docs/components/stepper.md +2 -0
  64. package/public/docs/components/switch.md +4 -2
  65. package/public/docs/components/tabs.md +2 -0
  66. package/public/docs/components/tag.md +2 -0
  67. package/public/docs/components/textarea.md +2 -0
  68. package/public/docs/components/timeline.md +2 -0
  69. package/public/docs/components/toast.md +2 -0
  70. package/public/docs/components/tool-call.md +2 -0
  71. package/public/docs/components/tooltip.md +2 -0
  72. package/public/manifest/components.json +16 -9
  73. package/public/manifest/tokens.json +36 -5
  74. package/scripts/division-ds.mjs +5 -5
  75. package/scripts/lib/agent-docs.mjs +28 -6
  76. package/scripts/lib/ai-copy.mjs +15 -1
  77. package/scripts/lib/check-text.mjs +13 -0
  78. package/scripts/lib/mcp-server.mjs +2 -1
  79. package/scripts/lib/mcp-tools.mjs +33 -8
  80. package/scripts/lib/props-validator.mjs +2 -1
  81. package/scripts/lib/token-discipline.mjs +10 -3
  82. package/skills/division-ds/commands/add.md +1 -1
  83. package/skills/division-ds/commands/build.md +1 -1
  84. package/skills/division-ds/commands/fix.md +1 -1
  85. package/skills/division-ds/commands/review.md +2 -2
  86. package/skills/division-ds/topics/ai.md +2 -1
  87. package/skills/division-ds/topics/forms.md +3 -0
  88. package/skills/division-ds/topics/layout.md +3 -1
  89. package/division-ds-0.2.0.tgz +0 -0
@@ -13,6 +13,8 @@
13
13
  import { Search } from 'src/components/molecules/search/Search'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { SectionHeading } from 'src/components/molecules/section-heading/SectionHeading'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { SegmentedControl } from 'src/components/molecules/segmented-control/SegmentedControl'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Select } from 'src/components/atoms/select/Select'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
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.
@@ -216,7 +218,7 @@ const TERRAIN_OPTIONS = [
216
218
  - **Trigger**: Uses exactly the same frame and styling as Input, so a select and a text field line up in a form.
217
219
  - **Value**: Shows the placeholder until something’s chosen.
218
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.
219
- - **Item**: Highlighted rows use signal.fill with signal.ink. That’s the fill-selection grammar, the system’s rule for how a selection looks.
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.
220
222
  - **ItemIndicator**: A tick on the selected row, so selection isn’t shown by the highlight alone.
221
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.
222
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.
@@ -13,6 +13,8 @@
13
13
  import { Sheet, SheetClose, SheetContent, SheetTrigger } from 'src/components/organisms/sheet/Sheet'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Sidebar } from 'src/components/organisms/sidebar/Sidebar'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Skeleton, SkeletonGroup } from 'src/components/atoms/skeleton/Skeleton'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Slider } from 'src/components/atoms/slider/Slider'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -108,5 +110,5 @@ function SingleValueLive() {
108
110
 
109
111
  - **Label and readout**: An 11px (pixel) mono label, in a monospaced font, and a 24px Oxanium readout with tabular figures, digits that all take the same width. Unlike Progress, there are no dimmed leading zeros. min and max belong to the caller and formatValue can return anything, so a fixed digit width would be one the value doesn’t actually have.
110
112
  - **Track**: 4px tall, sunken, no border.
111
- - **Range**: The filled span, in signal.fill.
113
+ - **Range**: The filled span, in accent.signal.indicator.
112
114
  - **Thumb**: A 20px square inside a 24px hit area, the region that responds to a click or tap. There’s one per value.
@@ -13,6 +13,8 @@
13
13
  import { Sparkline } from 'src/components/atoms/sparkline/Sparkline'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Spinner, spinnerVariants } from 'src/components/atoms/spinner/Spinner'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -32,7 +34,7 @@ Exported beside the component, from the same file. Import a type with `import ty
32
34
  export const spinnerVariants = cva(
33
35
  // The arc takes `currentColor`, so a caller can recolour it with one text utility: Button does
34
36
  // exactly that, so the arc matches whatever ink the button label is set in.
35
- 'inline-block shrink-0 text-[var(--color-accent-signal-fill)]',
37
+ 'inline-block shrink-0 text-[var(--color-accent-signal-indicator)]',
36
38
  {
37
39
  variants: {
38
40
  size: { sm: 'w-3.5 h-3.5', md: 'w-5 h-5', lg: 'w-6 h-6' },
@@ -91,6 +93,6 @@ Not focusable. No keyboard behaviour of its own.
91
93
  ## Anatomy
92
94
 
93
95
  - **span**: The root. It gets role="status" only when it's labelled.
94
- - **Ring (ring)**: An SVG (Scalable Vector Graphics) track at 20% of currentColor, which is the surrounding text colour, plus an arc in currentColor. That's accent.signal.fill by default.
96
+ - **Ring (ring)**: An SVG (Scalable Vector Graphics) track at 20% of currentColor, which is the surrounding text colour, plus an arc in currentColor. That's accent.signal.indicator by default.
95
97
  - **Blocks (conveyor)**: Three block glyphs, from full to half shade, running over a ten-cell track in border.control.
96
98
  - **sr-only label**: The text a screen reader announces. sr-only means it's hidden on screen but still read aloud. It's only there when label is passed.
@@ -13,6 +13,8 @@
13
13
  import { Stat } from 'src/components/molecules/stat/Stat'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { StatusTracker, statusTrackerText } from 'src/components/molecules/status-tracker/StatusTracker'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
20
  - The root TooltipProvider (AppShell and DemoLayout mount it).
@@ -13,6 +13,8 @@
13
13
  import { Stepper } from 'src/components/molecules/stepper/Stepper'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Switch } from 'src/components/atoms/switch/Switch'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
20
  - Its own label. A Switch isn't a FormField child (FormField takes Input, Textarea, Select or QuantityStepper). Give it an id and put a <label htmlFor> beside it.
@@ -147,5 +149,5 @@ FormField passes its child an invalid prop, and Switch doesn't take one. A switc
147
149
  ## Anatomy
148
150
 
149
151
  - **Root**: role=switch, which tells screen readers (software that reads the page aloud) what it is. A 44×24 track.
150
- - **Thumb**: A 16px (pixel) square, 3px from the track edge in both states, travelling 22px. Its position is the state signal. It's text.primary when off and accent.signal.fill when on. Held down, it stretches 4px away from the edge it rests on: to the right when off, to the left when on, so it never leaves the track. Flipped, it speeds up across the track, gets a little longer in flight and a little shorter against the far edge, then goes back to its own shape. One give, no bounce.
151
- - **Track fill**: The control fill, on and off. When it's on, only the border turns to signal.fill. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip.
152
+ - **Thumb**: A 16px (pixel) square, 3px from the track edge in both states, travelling 22px. Its position is the state signal. It's text.primary when off and accent.signal.indicator when on. Held down, it stretches 4px away from the edge it rests on: to the right when off, to the left when on, so it never leaves the track. Flipped, it speeds up across the track, gets a little longer in flight and a little shorter against the far edge, then goes back to its own shape. One give, no bounce.
153
+ - **Track fill**: The control fill, on and off. When it's on, only the border turns to accent.signal.indicator. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip.
@@ -13,6 +13,8 @@
13
13
  import { Tabs, TabsContent, TabsList, TabsTrigger } from 'src/components/molecules/tabs/Tabs'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Tag } from 'src/components/atoms/tag/Tag'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Textarea, textareaVariants } from 'src/components/atoms/textarea/Textarea'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
20
  - A <FormField> around it. On its own the control has no label. FormField supplies the label, the help text and the error, and links up their ids.
@@ -13,6 +13,8 @@
13
13
  import { Timeline, TimelineGroup, TimelineItem } from 'src/components/molecules/timeline/Timeline'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Toaster, useToast } from 'src/components/molecules/toast/Toast'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
20
  - One <Toaster> near the root of the app, wrapped around everything that calls useToast(). AppShell and the demo layout each mount one. Outside it, useToast() throws an error.
@@ -13,6 +13,8 @@
13
13
  import { ToolCall } from 'src/components/molecules/tool-call/ToolCall'
14
14
  ```
15
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
+
16
18
  ## Props
17
19
 
18
20
  | Prop | Type | Default | Description |
@@ -13,6 +13,8 @@
13
13
  import { Tooltip, TooltipProvider } from 'src/components/atoms/tooltip/Tooltip'
14
14
  ```
15
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
+
16
18
  ## Requires
17
19
 
18
20
  - One <TooltipProvider> near the root of the app. AppShell and the demo layout each mount one, so a screen doesn't add its own.
@@ -1537,7 +1537,7 @@
1537
1537
  "name": "tone",
1538
1538
  "type": "'signal' | 'ok' | 'warning' | 'hostile'",
1539
1539
  "default": "'signal'",
1540
- "description": "The fill colour. signal paints accent.signal.fill to match Slider. On the track in light theme that’s a 2.15:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there."
1540
+ "description": "The fill colour. signal paints accent.signal.indicator to match Slider. On the track in light theme that’s a 2.15:1 contrast ratio, below the 3:1 a bar needs when it’s the only sign of progress. So set showValue there."
1541
1541
  },
1542
1542
  {
1543
1543
  "name": "className",
@@ -1738,7 +1738,7 @@
1738
1738
  }
1739
1739
  ],
1740
1740
  "dos": [
1741
- "Name the group with aria-label or a FormField legend.",
1741
+ "Name the group: in a form, a fieldset legend that the RadioGroup points at with aria-labelledby; elsewhere, aria-label.",
1742
1742
  "Offer a default when one option really is the norm.",
1743
1743
  "Keep the set short. Past about seven options, use a Select."
1744
1744
  ],
@@ -1747,6 +1747,13 @@
1747
1747
  "Don't put a radio outside a RadioGroup. Both the roving focus and the value come from the group.",
1748
1748
  "Don't make choosing an option perform the action. Put a button after the group."
1749
1749
  ],
1750
+ "pitfalls": [
1751
+ {
1752
+ "wrong": "<FormField label=\"Difficulty\" error={error}>\n <RadioGroup>\n <Radio value=\"easy\" />\n </RadioGroup>\n</FormField>",
1753
+ "right": "<fieldset aria-describedby={error ? 'difficulty-error' : undefined}>\n <legend id=\"difficulty-label\" className=\"font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-primary)]\">\n Difficulty\n </legend>\n <RadioGroup aria-labelledby=\"difficulty-label\" aria-required=\"true\" value={value} onValueChange={setValue} className=\"mt-2\">\n {['easy', 'moderate', 'hard'].map(option => (\n <div key={option} className=\"flex items-center gap-2\">\n <Radio id={`difficulty-${option}`} value={option} invalid={Boolean(error)} />\n <label htmlFor={`difficulty-${option}`} className=\"font-body text-sm\">{option}</label>\n </div>\n ))}\n </RadioGroup>\n {error ? (\n <p id=\"difficulty-error\" className=\"mt-1.5 font-body text-sm text-[var(--color-accent-hostile-text)]\">{error}</p>\n ) : null}\n</fieldset>",
1754
+ "why": "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."
1755
+ }
1756
+ ],
1750
1757
  "anatomy": [
1751
1758
  {
1752
1759
  "part": "RadioGroup",
@@ -2114,7 +2121,7 @@
2114
2121
  },
2115
2122
  {
2116
2123
  "part": "Item",
2117
- "description": "Highlighted rows use signal.fill with signal.ink. That’s the fill-selection grammar, the system’s rule for how a selection looks."
2124
+ "description": "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."
2118
2125
  },
2119
2126
  {
2120
2127
  "part": "ItemIndicator",
@@ -2446,7 +2453,7 @@
2446
2453
  },
2447
2454
  {
2448
2455
  "part": "Range",
2449
- "description": "The filled span, in signal.fill."
2456
+ "description": "The filled span, in accent.signal.indicator."
2450
2457
  },
2451
2458
  {
2452
2459
  "part": "Thumb",
@@ -2669,7 +2676,7 @@
2669
2676
  {
2670
2677
  "name": "spinnerVariants",
2671
2678
  "kind": "const",
2672
- "code": "export const spinnerVariants = cva(\n // The arc takes `currentColor`, so a caller can recolour it with one text utility: Button does\n // exactly that, so the arc matches whatever ink the button label is set in.\n 'inline-block shrink-0 text-[var(--color-accent-signal-fill)]',\n {\n variants: {\n size: { sm: 'w-3.5 h-3.5', md: 'w-5 h-5', lg: 'w-6 h-6' },\n },\n defaultVariants: { size: 'md' },\n }\n)",
2679
+ "code": "export const spinnerVariants = cva(\n // The arc takes `currentColor`, so a caller can recolour it with one text utility: Button does\n // exactly that, so the arc matches whatever ink the button label is set in.\n 'inline-block shrink-0 text-[var(--color-accent-signal-indicator)]',\n {\n variants: {\n size: { sm: 'w-3.5 h-3.5', md: 'w-5 h-5', lg: 'w-6 h-6' },\n },\n defaultVariants: { size: 'md' },\n }\n)",
2673
2680
  "from": "src/components/atoms/spinner/Spinner"
2674
2681
  }
2675
2682
  ],
@@ -2692,7 +2699,7 @@
2692
2699
  },
2693
2700
  {
2694
2701
  "part": "Ring (ring)",
2695
- "description": "An SVG (Scalable Vector Graphics) track at 20% of currentColor, which is the surrounding text colour, plus an arc in currentColor. That's accent.signal.fill by default."
2702
+ "description": "An SVG (Scalable Vector Graphics) track at 20% of currentColor, which is the surrounding text colour, plus an arc in currentColor. That's accent.signal.indicator by default."
2696
2703
  },
2697
2704
  {
2698
2705
  "part": "Blocks (conveyor)",
@@ -2843,11 +2850,11 @@
2843
2850
  },
2844
2851
  {
2845
2852
  "part": "Thumb",
2846
- "description": "A 16px (pixel) square, 3px from the track edge in both states, travelling 22px. Its position is the state signal. It's text.primary when off and accent.signal.fill when on. Held down, it stretches 4px away from the edge it rests on: to the right when off, to the left when on, so it never leaves the track. Flipped, it speeds up across the track, gets a little longer in flight and a little shorter against the far edge, then goes back to its own shape. One give, no bounce."
2853
+ "description": "A 16px (pixel) square, 3px from the track edge in both states, travelling 22px. Its position is the state signal. It's text.primary when off and accent.signal.indicator when on. Held down, it stretches 4px away from the edge it rests on: to the right when off, to the left when on, so it never leaves the track. Flipped, it speeds up across the track, gets a little longer in flight and a little shorter against the far edge, then goes back to its own shape. One give, no bounce."
2847
2854
  },
2848
2855
  {
2849
2856
  "part": "Track fill",
2850
- "description": "The control fill, on and off. When it's on, only the border turns to signal.fill. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip."
2857
+ "description": "The control fill, on and off. When it's on, only the border turns to accent.signal.indicator. The track doesn't fill with the accent, so the thumb is the one thing that changes colour during a flip."
2851
2858
  }
2852
2859
  ],
2853
2860
  "examples": [
@@ -4917,7 +4924,7 @@
4917
4924
  {
4918
4925
  "name": "children",
4919
4926
  "type": "ReactElement",
4920
- "description": "Exactly one control: Input, Textarea, Select or QuantityStepper. A label set on a Select or QuantityStepper child is replaced by the FormField label."
4927
+ "description": "Exactly one control: Input, Textarea, Select or QuantityStepper. A label set on a Select or QuantityStepper child is replaced by the FormField label. An id you give the control is kept (point an ErrorSummary entry at it); without one, FormField makes one up."
4921
4928
  },
4922
4929
  {
4923
4930
  "name": "className",
@@ -564,7 +564,11 @@
564
564
  "name": "--color-accent-signal-fill",
565
565
  "layer": "semantic",
566
566
  "value": "#f2550f",
567
- "use": "A solid block in the brand orange: the primary Button, a solid signal Badge. Put accent.signal.ink on it, never text.primary."
567
+ "use": "A solid block in the brand colour, exactly as chosen: the primary Button, IconButton, a solid signal Badge. Put accent.signal.ink on it, never text.primary. Never use it as the only sign of a state; use accent.signal.indicator.",
568
+ "measured": {
569
+ "dark": "The brand colour, exactly as chosen: Button, IconButton and solid Badge backgrounds, and brand decoration. Text on it is accent.signal.ink. Never the only sign of a state; that is accent.signal.indicator. The theme builder never moves it.",
570
+ "light": "The brand colour, exactly as chosen: Button, IconButton and solid Badge backgrounds, and brand decoration. Text on it is accent.signal.ink. Never the only sign of a state; that is accent.signal.indicator. The theme builder never moves it."
571
+ }
568
572
  },
569
573
  {
570
574
  "name": "--color-accent-signal-text",
@@ -585,7 +589,28 @@
585
589
  "value": "#0a0602",
586
590
  "use": "Text and icons on accent.signal.fill.",
587
591
  "measured": {
588
- "dark": "5.85:1 on accent.signal.fill"
592
+ "dark": "5.85:1 on accent.signal.fill",
593
+ "light": "5.85:1 on accent.signal.fill"
594
+ }
595
+ },
596
+ {
597
+ "name": "--color-accent-signal-indicator",
598
+ "layer": "semantic",
599
+ "value": "#f2550f",
600
+ "use": "The accent where colour alone shows a state or value: a checked control, Switch on, Progress, Slider, Rating stars, the current item in Sidebar, Navbar, Tabs, SegmentedControl, Stepper and highlighted menu rows. Put accent.signal.indicator-ink on it.",
601
+ "measured": {
602
+ "dark": "The accent where colour alone marks a state or a value: checked controls, Switch on, Progress, Slider, Rating stars, the current item of Sidebar, Navbar, Tabs, SegmentedControl, Stepper and highlighted menu rows, and the hero numerals. 5.80:1 on surface.base. Same stop as accent.signal.fill here; the theme builder moves it to a nearby stop when a chosen accent is too pale or too dark to reach 3:1, and leaves accent.signal.fill alone.",
603
+ "light": "The accent where colour alone marks a state or a value: checked controls, Switch on, Progress, Slider, Rating stars, the current item of Sidebar, Navbar, Tabs, SegmentedControl, Stepper and highlighted menu rows, and the hero numerals. 3.14:1 on surface.base. Same stop as accent.signal.fill here; the theme builder moves it to a nearby stop when a chosen accent is too pale or too dark to reach 3:1, and leaves accent.signal.fill alone."
604
+ }
605
+ },
606
+ {
607
+ "name": "--color-accent-signal-indicator-ink",
608
+ "layer": "semantic",
609
+ "value": "#0a0602",
610
+ "use": "Text and icons on accent.signal.indicator.",
611
+ "measured": {
612
+ "dark": "Text and icons on accent.signal.indicator. 5.85:1. The theme builder picks white or this dark ink, whichever reaches 4.5:1.",
613
+ "light": "Text and icons on accent.signal.indicator. 5.85:1. The theme builder picks white or this dark ink, whichever reaches 4.5:1."
589
614
  }
590
615
  },
591
616
  {
@@ -595,7 +620,7 @@
595
620
  "use": "The faint orange hover wash under a nav item or an inactive tab.",
596
621
  "measured": {
597
622
  "dark": "Hover wash under a nav item or an inactive tab. Text on it is text.primary and gated. The focus ring and a checked control's fill drop under 3:1 on it in light theme, so neither is ever drawn on it.",
598
- "light": "Hover wash under a nav item or an inactive tab. Text on it is text.primary and gated. The focus ring and a checked control's fill drop under 3:1 on it in light theme, so neither is ever drawn on it."
623
+ "light": "Hover wash under a nav item or an inactive tab. Text on it is text.primary and gated. A checked control's indicator drops under 3:1 on it in light theme and the focus ring is not gated on it, so neither is ever drawn on it."
599
624
  }
600
625
  },
601
626
  {
@@ -782,8 +807,14 @@
782
807
  {
783
808
  "name": "--color-focus-ring",
784
809
  "layer": "semantic",
785
- "value": "#f2550f",
786
- "use": "The keyboard focus outline. Use it only in focus-visible:outline-[var(--color-focus-ring)], with a 2px outline and a 2px offset."
810
+ "value": {
811
+ "dark": "#f2550f",
812
+ "light": "#c6440b"
813
+ },
814
+ "use": "The keyboard focus outline. Use it only in focus-visible:outline-[var(--color-focus-ring)], with a 2px outline and a 2px offset.",
815
+ "measured": {
816
+ "light": "4.51:1 on surface.base. A step darker than accent.signal.fill, which reaches only 3.14:1 on the light page: the ring is the one mark every keyboard user depends on, so it gets a margin."
817
+ }
787
818
  },
788
819
  {
789
820
  "name": "--color-chart-series-1",
@@ -19,7 +19,7 @@
19
19
  import { existsSync, globSync, readdirSync, readFileSync, statSync } from 'node:fs'
20
20
  import { dirname, join, relative, resolve } from 'node:path'
21
21
  import { fileURLToPath } from 'node:url'
22
- import { extractRules } from './lib/agent-docs.mjs'
22
+ import { DOCS_SITE, extractRules, SKILL_SOURCE } from './lib/agent-docs.mjs'
23
23
  import { checkText } from './lib/check-text.mjs'
24
24
  import { createServer } from './lib/mcp-server.mjs'
25
25
  import { createTools } from './lib/mcp-tools.mjs'
@@ -28,14 +28,14 @@ import { applyPlan, diff, planRules, planSkill, SKILL_TARGETS, TARGETS } from '.
28
28
 
29
29
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
30
30
  const DEFAULT_MANIFEST = join(ROOT, 'public/manifest/components.json')
31
- const CHECKED = /\.(tsx|jsx|ts)$/
31
+ const CHECKED = /\.(tsx|jsx|ts|css)$/
32
32
  const SKIPPED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage'])
33
33
 
34
34
  const HELP = `division-ds: Division DS gates and agent rules for a project that uses the components.
35
35
 
36
36
  Usage:
37
37
  division-ds check [paths...] [--manifest <path|url>] [--tokens <path|url>] [--json]
38
- Checks .tsx, .jsx and .ts files (default: ./src). A path may be a file, a directory or a
38
+ Checks .tsx, .jsx, .ts and .css files (default: ./src; a .css file gets the colour gate only). A path may be a file, a directory or a
39
39
  glob ("src/**/*.tsx"). Runs five gates and exits 1 on any finding:
40
40
  token-discipline raw colours, Tailwind palette classes, token names that do not exist
41
41
  button-spacing two Buttons/IconButtons side by side with no gap on their parent
@@ -149,12 +149,12 @@ async function check(options) {
149
149
  if (gate === 'button-spacing') console.error(` ${ofGate[0].fix}`)
150
150
  console.error('')
151
151
  }
152
- console.error('Each component\'s props, values and examples: /docs/components/<slug>.md. Tokens: public/manifest/tokens.json.')
152
+ console.error(`Each component's props, values and examples: ${DOCS_SITE}docs/components/<slug>.md. Tokens: ${DOCS_SITE}llms-tokens.txt.`)
153
153
  } else {
154
154
  console.log(`division-ds check passed: ${files.length} file(s), ${index.size} components, ${tokenNames.length} tokens, 0 findings.`)
155
155
  // It reads code, not the running page: say so, or a pass reads as "the screen works" (the
156
156
  // 2026-10-20 outside test shipped three runtime bugs past a clean check).
157
- console.log('Checked: colours, tokens, button spacing, inline host cards, props and AI copy. Not checked: how the screen behaves; run the review checklist (commands/review.md) in a browser.')
157
+ console.log(`Checked: colours, tokens, button spacing, inline host cards, props and AI copy. Not checked: how the screen behaves; run the review checklist in a browser (commands/review.md from \`npx division-ds skill\`, or ${SKILL_SOURCE}commands/review.md).`)
158
158
  }
159
159
  process.exit(findings.length > 0 ? 1 : 0)
160
160
  }
@@ -8,6 +8,8 @@ const TIER_NOUN = { atoms: 'Atom', molecules: 'Molecule', organisms: 'Organism'
8
8
  export const DOCS_SITE = 'https://victor-ilioiu.vercel.app/systems/division-ds/'
9
9
  /** Where shadcn downloads the components from: public/r/ in the public GitHub repository. */
10
10
  export const REGISTRY = 'https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r'
11
+ /** The skill's files in the public repository, for a reader who has the package but not the skill. */
12
+ export const SKILL_SOURCE = 'https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/'
11
13
 
12
14
  /** A table cell: one line, with the pipes that would end the cell escaped. */
13
15
  const cell = text => String(text ?? '').replace(/\s*\n\s*/g, ' ').replace(/\|/g, '\\|')
@@ -19,14 +21,29 @@ export function importLines(entry) {
19
21
  return entry.imports.map(({ names, from }) => `import { ${names.join(', ')} } from '${from}'`).join('\n')
20
22
  }
21
23
 
24
+ /** The other components a page documents, by export name: ChatMessage's ResponseFeedback,
25
+ * ProductCard's ProductGrid. Parts named after the component (CardBody, RadioGroup) and helpers
26
+ * (lower case) are left out. The 2026-10-29 outside test could not find ResponseFeedback because
27
+ * only its parent was listed anywhere an agent looks first. */
28
+ export function alsoExports(entry) {
29
+ return entry.imports
30
+ .flatMap(({ names }) => names)
31
+ .filter(name => /^[A-Z][a-z]/.test(name) && !name.startsWith(entry.name) && !name.endsWith('Provider'))
32
+ }
33
+
34
+ /** Said under every Import block: the paths are the repository's, not an installed project's. */
35
+ export const INSTALLED_PATHS = '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.'
36
+
22
37
  /** One component as a markdown page. `entry` is a manifest entry (see `manifestEntry`). */
23
38
  export function componentMarkdown(entry) {
24
39
  const out = [`# ${entry.name}`, '', `> ${entry.summary}`, '']
25
40
  out.push(`- **Best for:** ${entry.useWhen}`, `- **Not for:** ${entry.notFor}`)
41
+ const also = alsoExports(entry)
42
+ if (also.length) out.push(`- **Also on this page:** ${also.map(code).join(', ')}`)
26
43
  out.push(`- ${TIER_NOUN[entry.tier]}. Source: \`${entry.source}\``)
27
44
  out.push(`- Rendered docs page: \`/components/${entry.slug}\``, '')
28
45
 
29
- out.push('## Import', '', fence(importLines(entry)), '')
46
+ out.push('## Import', '', fence(importLines(entry)), '', INSTALLED_PATHS, '')
30
47
 
31
48
  if (entry.requires?.length) {
32
49
  out.push('## Requires', '', ...entry.requires.map(item => `- ${item}`), '')
@@ -108,8 +125,10 @@ const byTier = entries =>
108
125
  export const firstSentence = text => text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text
109
126
 
110
127
  /** One component's line in llms.txt: what it is, then best for and not for. */
111
- export const indexLine = entry =>
112
- `- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)} Best for: ${entry.useWhen} Not for: ${entry.notFor}`
128
+ export const indexLine = entry => {
129
+ const also = alsoExports(entry)
130
+ return `- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)} Best for: ${entry.useWhen} Not for: ${entry.notFor}${also.length ? ` Also: ${also.join(', ')}.` : ''}`
131
+ }
113
132
 
114
133
  /** llms.txt, in the llmstxt.org shape: a title, a summary, then sections of links. */
115
134
  export function llmsIndex(entries, { rules }) {
@@ -245,7 +264,10 @@ export function designFile(entries, manifest, { fonts, rules }) {
245
264
  ]
246
265
  for (const [tier, group] of byTier(entries)) {
247
266
  out.push(`## ${tier[0].toUpperCase()}${tier.slice(1)}`, '')
248
- for (const entry of group) out.push(`- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)}`)
267
+ for (const entry of group) {
268
+ const also = alsoExports(entry)
269
+ out.push(`- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)}${also.length ? ` Also: ${also.join(', ')}.` : ''}`)
270
+ }
249
271
  out.push('')
250
272
  }
251
273
  out.push('## Rules', '', 'In full: [llms-guidelines.txt](llms-guidelines.txt).', '', ...ruleHeadlines(rules).map(line => `- ${line}`), '')
@@ -462,7 +484,7 @@ const SKILL_COMMANDS = {
462
484
  body: [
463
485
  'Go through every line. A line that does not hold is a change to make, not a note to leave.',
464
486
  '',
465
- '- [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned in `commands/build.md`. An empty list says what is empty and offers the action that fills it.',
487
+ '- [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned before the code (the regions-and-states plan of `commands/build.md`). An empty list says what is empty and offers the action that fills it.',
466
488
  '- [ ] **Headings.** One `PageHeader`, so one `h1`. Section headings continue the outline: `h2` under the `h1`, `h3` inside an `h2`, no level skipped. `SectionHeading` is given the level.',
467
489
  '- [ ] **Tokens only.** No hex, `rgb()`, palette class or invented token name. `division-ds check` passes.',
468
490
  '- [ ] **Buttons.** Two or more side by side share a parent with a gap. One primary button per view. A busy button takes `loading`; a disabled one says why nearby. Icon-only actions are `IconButton` with a `label`.',
@@ -557,7 +579,7 @@ export function skillFiles(split, componentCount) {
557
579
  '',
558
580
  // A project that installed the components has no public/ folder of ours (the 2026-10-20
559
581
  // outside test followed these steps to files that did not exist).
560
- `Paths starting \`public/\` are in the Division DS repository. In a project that uses the components, read them online: \`public/llms.txt\` is \`${DOCS_SITE}llms.txt\`, and so on.`,
582
+ `Paths starting \`public/\` are in the Division DS repository. In a project that uses the components, read them online: \`public/llms.txt\` is \`${DOCS_SITE}llms.txt\`, and so on. The other procedures named here (\`commands/build.md\` and the rest) are in the skill: \`${SKILL_SOURCE}commands/<name>.md\`, or \`npx division-ds skill\` to install them.`,
561
583
  '',
562
584
  ...command.body,
563
585
  '',
@@ -16,7 +16,9 @@
16
16
  // - `assistant-copy`: "answer", "please" or "!" in the interface text of the assistant components
17
17
  // (ChatThread, ChatMessage, PromptInput, ApprovalRequest, ToolCall): their label-like string
18
18
  // props only. Message bodies, a ToolCall's input and output and PromptInput's suggestions are
19
- // content written by the person or the model, not the interface, and are never read.
19
+ // content written by the person or the model, not the interface, and are never read. A button
20
+ // placed inside one of them (a Button in ChatMessage's `actions`) is its interface too: its label
21
+ // and aria-label are checked the same way (missed by the 2026-10-29 outside test's probe).
20
22
  // Only literals are read (a string attribute, JSX text, a ternary of two literals). A variable is
21
23
  // not judged, the same as the props gate.
22
24
  import ts from 'typescript'
@@ -63,11 +65,22 @@ export function findAiCopyViolations(text, path) {
63
65
  const at = source.getLineAndCharacterOfPosition(node.getStart(source))
64
66
  violations.push({ path, line: at.line + 1, column: at.character + 1, rule, found, fix })
65
67
  }
68
+ // How many assistant components the walk is inside: a button in there speaks for the assistant.
69
+ let insideAssistant = 0
66
70
  const checkCta = (node, value, where) => {
67
71
  if (AI_WORD.test(value)) add(node, 'ai-in-cta', `${where}: "${value.trim()}"`, AI_FIX)
72
+ if (insideAssistant > 0) {
73
+ for (const word of ASSISTANT_WORDS) {
74
+ if (word.pattern.test(value)) add(node, 'assistant-copy', `${where} in an assistant component: "${value.trim()}"`, word.fix)
75
+ }
76
+ }
68
77
  }
78
+ const tagOf = node =>
79
+ ts.isJsxElement(node) ? node.openingElement.tagName.getText(source) : ts.isJsxSelfClosingElement(node) ? node.tagName.getText(source) : undefined
69
80
 
70
81
  const visit = node => {
82
+ const opensAssistant = tagOf(node) in ASSISTANT_COPY
83
+ if (opensAssistant) insideAssistant++
71
84
  if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
72
85
  const tag = node.tagName.getText(source)
73
86
  for (const attribute of node.attributes.properties) {
@@ -96,6 +109,7 @@ export function findAiCopyViolations(text, path) {
96
109
  if (label.trim()) checkCta(node, label, `<${tag}> label`)
97
110
  }
98
111
  ts.forEachChild(node, visit)
112
+ if (opensAssistant) insideAssistant--
99
113
  }
100
114
  visit(source)
101
115
  return violations.sort((a, b) => a.line - b.line || a.column - b.column)
@@ -10,10 +10,23 @@ import { findViolations as tokenDiscipline, formatViolation as formatTokenDiscip
10
10
  export const BUTTON_SPACING_FIX =
11
11
  'Wrap the buttons in one parent with a flex/grid class and a gap- utility (flex flex-wrap items-center gap-3), and move any margin from the buttons onto it.'
12
12
 
13
+ /** A stylesheet with the value of every declaration of a real token blanked, newlines kept so line
14
+ * numbers hold. Setting a token is how a project themes the system (the theme builder writes
15
+ * exactly that into index.css), so a literal colour there is the point; anywhere else in the file,
16
+ * or on a custom property the system does not have, it is reported as in any other file. */
17
+ export function blankTokenOverrides(text, tokenNames) {
18
+ const known = new Set(tokenNames)
19
+ return text.replace(/(--[a-z0-9-]+)(\s*:)([^;}]*)/g, (match, name, colon, value) =>
20
+ // The var() references stay, so a token set from a name that does not exist is still caught.
21
+ known.has(name) ? name + colon + value.replace(/var\([^)]*\)|[^\n]/g, part => (part.length > 1 ? part : ' ')) : match
22
+ )
23
+ }
24
+
13
25
  /** Every finding in `text`. `path` decides which gates apply (button spacing and AI copy need JSX) and is
14
26
  * echoed into each finding. `index` is buildIndex(manifest); `tokenNames` the token names. */
15
27
  export function checkText(text, path, index, tokenNames) {
16
28
  const findings = []
29
+ if (/\.css$/.test(path)) text = blankTokenOverrides(text, tokenNames)
17
30
  for (const v of tokenDiscipline(text, path, tokenNames)) {
18
31
  findings.push({ gate: 'token-discipline', path, line: v.line, rule: v.rule, found: v.found, fix: v.fix, text: formatTokenDiscipline(v) })
19
32
  }
@@ -12,7 +12,8 @@ export const PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '202
12
12
 
13
13
  const INSTRUCTIONS = `Division DS: a React 19, Tailwind 4 and Radix design system.
14
14
  Before writing a component, call get_component for its props and examples. Use only tokens from get_tokens or search, as var(--name).
15
- Before saying a screen is done, call validate_code on each file you wrote and fix every finding, then go through get_review_checklist.`
15
+ Before saying a screen is done, call validate_code on each file you wrote (.css too) and fix every finding, then go through get_review_checklist.
16
+ Shared helpers (theme toggle, form errors, money, links): https://victor-ilioiu.vercel.app/systems/division-ds/docs/helpers.md`
16
17
 
17
18
  /** A JSON-RPC error reply. */
18
19
  const failure = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } })