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
@@ -1,6 +1,125 @@
1
1
  {
2
2
  "generatedBy": "scripts/build-agent-docs.mjs",
3
3
  "components": [
4
+ {
5
+ "name": "AiLabel",
6
+ "title": "AI label",
7
+ "tier": "atoms",
8
+ "slug": "ai-label",
9
+ "summary": "Marks content as made by AI, and on request says why. Given an explanation, the mark is a button that opens a short note right under it, with an optional list of sources. It is a disclosure (a button that shows and hides a region), not a tooltip or a popover: a tooltip can't hold links and never shows on a touch screen, and the system has no popover.",
10
+ "useWhen": "Marking a block of text, a value or a field that an AI wrote, so the person knows to check it.",
11
+ "notFor": "A status or a category: use Badge. A field the AI filled: give the field aiValue, which marks it itself.",
12
+ "source": "src/components/atoms/ai-label/AiLabel.tsx",
13
+ "docs": "/docs/components/ai-label.md",
14
+ "imports": [
15
+ {
16
+ "names": [
17
+ "AiLabel"
18
+ ],
19
+ "from": "src/components/atoms/ai-label/AiLabel"
20
+ }
21
+ ],
22
+ "props": [
23
+ {
24
+ "name": "label",
25
+ "type": "string",
26
+ "default": "'Generated by AI'",
27
+ "description": "The mark's words, and the button's name. Say what the AI made: 'AI summary', 'AI draft'."
28
+ },
29
+ {
30
+ "name": "explanation",
31
+ "type": "ReactNode",
32
+ "description": "Why this is AI content and what to do about it, in a sentence or two: what it was made from, and that it needs checking. Without it the mark is static text and nothing opens."
33
+ },
34
+ {
35
+ "name": "sources",
36
+ "type": "AiLabelSource[]",
37
+ "description": "{ label, href } for each thing the content was made from. Listed under the explanation; ignored without one."
38
+ },
39
+ {
40
+ "name": "renderLink",
41
+ "type": "(props: LinkRenderProps) => ReactNode",
42
+ "default": "a plain anchor",
43
+ "description": "How each source link is drawn. Pass your router's link component for sources inside the app."
44
+ },
45
+ {
46
+ "name": "className",
47
+ "type": "string",
48
+ "description": "Merged with the root's classes. When Tailwind utility classes clash, the later one wins."
49
+ },
50
+ {
51
+ "name": "ref",
52
+ "type": "Ref<HTMLDivElement>",
53
+ "description": "Passed through to the root div."
54
+ },
55
+ {
56
+ "name": "...rest",
57
+ "type": "HTMLAttributes<HTMLDivElement>",
58
+ "description": "Every native attribute, onto the root div."
59
+ }
60
+ ],
61
+ "api": [
62
+ {
63
+ "name": "AiLabelSource",
64
+ "kind": "type",
65
+ "code": "export type AiLabelSource = { label: string; href: string }",
66
+ "from": "src/components/atoms/ai-label/AiLabel"
67
+ }
68
+ ],
69
+ "keyboard": [
70
+ {
71
+ "keys": [
72
+ "Tab"
73
+ ],
74
+ "does": "Moves focus to the mark when it has an explanation, then into any source links once open."
75
+ },
76
+ {
77
+ "keys": [
78
+ "Enter",
79
+ "Space"
80
+ ],
81
+ "does": "Opens or closes the explanation. Focus stays on the button."
82
+ }
83
+ ],
84
+ "dos": [
85
+ "Put it right above or beside the AI content it marks, not in a page header far away.",
86
+ "Say in the explanation what the AI used and that the person should check it, the way GitLab's 'Verify before use' does.",
87
+ "Name the sources when there are any. Showing where it came from is how a reader decides whether to trust it."
88
+ ],
89
+ "donts": [
90
+ "Don't put it inside a <label>. A button inside a label is read into the field's name; put it beside the label.",
91
+ "Don't mark everything. A label on every line is a label on nothing.",
92
+ "Don't rely on the colour. The words say AI; ai.text only helps you find them."
93
+ ],
94
+ "anatomy": [
95
+ {
96
+ "part": "Mark",
97
+ "description": "A sparkle and the label in uppercase mono, in ai.text. Static text when there is no explanation."
98
+ },
99
+ {
100
+ "part": "Disclosure button",
101
+ "description": "With an explanation, the mark is a button with aria-expanded and a chevron that turns when open. Its name is the label."
102
+ },
103
+ {
104
+ "part": "Explanation",
105
+ "description": "A region right under the button, edged in ai.border, in text.secondary. Hidden until asked for, then next in the reading order."
106
+ },
107
+ {
108
+ "part": "Sources",
109
+ "description": "Optional. A list of links under the explanation, drawn through renderLink."
110
+ }
111
+ ],
112
+ "examples": [
113
+ {
114
+ "caption": "Static mark",
115
+ "source": "<AiLabel />"
116
+ },
117
+ {
118
+ "caption": "With an explanation and sources",
119
+ "source": "<div className=\"flex flex-col gap-2 max-w-md\">\n <AiLabel\n label=\"AI summary\"\n explanation=\"Summarised from the three most recent reviews of this replica. It can miss details or get them wrong: verify before you rely on it.\"\n sources={[\n { label: 'Review by Ilse Varga', href: '#review-1' },\n { label: 'Review by Tomas Brandt', href: '#review-2' },\n ]}\n />\n <p className=\"text-sm text-[var(--color-text-primary)]\">\n Reviewers find the gearbox quiet and the hop-up easy to tune; two mention a loose stock.\n </p>\n</div>"
120
+ }
121
+ ]
122
+ },
4
123
  {
5
124
  "name": "Avatar",
6
125
  "title": "Avatar",
@@ -952,6 +1071,11 @@
952
1071
  "default": "'Clear'",
953
1072
  "description": "The clear button’s name. If the page has more than one, say what it clears: ‘Clear call sign’."
954
1073
  },
1074
+ {
1075
+ "name": "aiValue",
1076
+ "type": "string",
1077
+ "description": "The value an AI proposed. While the field holds exactly this value it shows the AI-filled state: an ai.border edge, a faint ai.wash tint and a sparkle mark that describes the field as Filled by AI. Once the person changes it, the field looks normal again and a Revert to suggestion button puts the value back through onChange (no button on a disabled or read-only field). An invalid field keeps its invalid border."
1078
+ },
955
1079
  {
956
1080
  "name": "wrapperClassName",
957
1081
  "type": "string",
@@ -986,7 +1110,7 @@
986
1110
  "keys": [
987
1111
  "Tab"
988
1112
  ],
989
- "does": "Moves focus to the input, then to its clear button and reveal toggle if they are there."
1113
+ "does": "Moves focus to the input, then to its revert, clear and reveal buttons if they are there."
990
1114
  },
991
1115
  {
992
1116
  "keys": [
@@ -1015,7 +1139,8 @@
1015
1139
  "Don't set invalid without saying what's wrong.",
1016
1140
  "Don't disable a field to make it read-only. Use readOnly, which you can still focus and copy from.",
1017
1141
  "Don't add clearable to a search field. Search already has its own clear button and the Escape key.",
1018
- "Don't build your own show-password button next to a revealable field. You'll end up with two."
1142
+ "Don't build your own show-password button next to a revealable field. You'll end up with two.",
1143
+ "Don't set aiValue on a field the person filled in themselves. The AI look says the machine wrote it."
1019
1144
  ],
1020
1145
  "anatomy": [
1021
1146
  {
@@ -1032,7 +1157,7 @@
1032
1157
  },
1033
1158
  {
1034
1159
  "part": "Adornments",
1035
- "description": "Optional extras inside the right edge, in this order: the count in mono (a fixed-width typeface), the clear button and the reveal toggle. Turning on any of them wraps the input in a relative box (wrapperClassName styles it) and sizes the right padding to fit them, so text never runs underneath. With none on, the input is bare, with no wrapper."
1160
+ "description": "Optional extras inside the right edge, in this order: the AI mark or its revert button, the count in mono (a fixed-width typeface), the clear button and the reveal toggle. Turning on any of them wraps the input in a relative box (wrapperClassName styles it) and sizes the right padding to fit them, so text never runs underneath. With none on, the input is bare, with no wrapper."
1036
1161
  },
1037
1162
  {
1038
1163
  "part": "Placeholder",
@@ -1064,6 +1189,10 @@
1064
1189
  "caption": "Clear button",
1065
1190
  "source": "<div className=\"w-full max-w-xs\">\n <FormField label=\"Call sign\">\n <Input clearable clearLabel=\"Clear call sign\" defaultValue=\"Anvil-3\" />\n </FormField>\n</div>"
1066
1191
  },
1192
+ {
1193
+ "caption": "Filled by AI, with a revert",
1194
+ "source": "<div className=\"w-full max-w-xs\">\n <FormField label=\"Call sign\" help=\"Suggested from your last order. Edit it and the revert button appears.\">\n <Input aiValue=\"Anvil-3\" defaultValue=\"Anvil-3\" />\n </FormField>\n</div>"
1195
+ },
1067
1196
  {
1068
1197
  "caption": "Labelled the manual way",
1069
1198
  "source": "<div className=\"flex flex-col gap-1 text-sm max-w-xs\">\n <label htmlFor=\"input-docs-callsign\">Call sign</label>\n <Input id=\"input-docs-callsign\" aria-describedby=\"input-docs-callsign-help\" />\n <p id=\"input-docs-callsign-help\" className=\"text-xs text-[var(--color-text-secondary)]\">\n Letters, numbers and a hyphen. FormField will set this pattern up for you.\n </p>\n</div>"
@@ -1408,7 +1537,7 @@
1408
1537
  "name": "tone",
1409
1538
  "type": "'signal' | 'ok' | 'warning' | 'hostile'",
1410
1539
  "default": "'signal'",
1411
- "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."
1412
1541
  },
1413
1542
  {
1414
1543
  "name": "className",
@@ -1609,7 +1738,7 @@
1609
1738
  }
1610
1739
  ],
1611
1740
  "dos": [
1612
- "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.",
1613
1742
  "Offer a default when one option really is the norm.",
1614
1743
  "Keep the set short. Past about seven options, use a Select."
1615
1744
  ],
@@ -1618,6 +1747,13 @@
1618
1747
  "Don't put a radio outside a RadioGroup. Both the roving focus and the value come from the group.",
1619
1748
  "Don't make choosing an option perform the action. Put a button after the group."
1620
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
+ ],
1621
1757
  "anatomy": [
1622
1758
  {
1623
1759
  "part": "RadioGroup",
@@ -1985,7 +2121,7 @@
1985
2121
  },
1986
2122
  {
1987
2123
  "part": "Item",
1988
- "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."
1989
2125
  },
1990
2126
  {
1991
2127
  "part": "ItemIndicator",
@@ -2317,7 +2453,7 @@
2317
2453
  },
2318
2454
  {
2319
2455
  "part": "Range",
2320
- "description": "The filled span, in signal.fill."
2456
+ "description": "The filled span, in accent.signal.indicator."
2321
2457
  },
2322
2458
  {
2323
2459
  "part": "Thumb",
@@ -2540,7 +2676,7 @@
2540
2676
  {
2541
2677
  "name": "spinnerVariants",
2542
2678
  "kind": "const",
2543
- "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)",
2544
2680
  "from": "src/components/atoms/spinner/Spinner"
2545
2681
  }
2546
2682
  ],
@@ -2563,7 +2699,7 @@
2563
2699
  },
2564
2700
  {
2565
2701
  "part": "Ring (ring)",
2566
- "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."
2567
2703
  },
2568
2704
  {
2569
2705
  "part": "Blocks (conveyor)",
@@ -2714,11 +2850,11 @@
2714
2850
  },
2715
2851
  {
2716
2852
  "part": "Thumb",
2717
- "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."
2718
2854
  },
2719
2855
  {
2720
2856
  "part": "Track fill",
2721
- "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."
2722
2858
  }
2723
2859
  ],
2724
2860
  "examples": [
@@ -2886,6 +3022,11 @@
2886
3022
  "default": "4",
2887
3023
  "description": "The browser's own rows attribute. Size it to the answer you expect."
2888
3024
  },
3025
+ {
3026
+ "name": "aiValue",
3027
+ "type": "string",
3028
+ "description": "The value an AI proposed, with the same contract as Input. The mark and the revert button sit in the top right corner. While the field holds exactly this value it shows the AI-filled state: an ai.border edge, a faint ai.wash tint and a sparkle mark that describes the field as Filled by AI. Once the person changes it, the field looks normal again and a Revert to suggestion button puts the value back through onChange (no button on a disabled or read-only field). An invalid field keeps its invalid border."
3029
+ },
2889
3030
  {
2890
3031
  "name": "className",
2891
3032
  "type": "string",
@@ -2962,6 +3103,10 @@
2962
3103
  "caption": "States",
2963
3104
  "source": "<label className=\"flex flex-col gap-1 text-sm w-full max-w-xs\">\n Invalid\n <Textarea invalid defaultValue=\"Site coordinates unclear\" />\n</label>\n<label className=\"flex flex-col gap-1 text-sm w-full max-w-xs\">\n Disabled\n <Textarea disabled defaultValue=\"Locked until review closes\" />\n</label>\n{/* read-only is a different state from disabled and looks different — the text stays\n selectable and the control stays in the tab order, so `read-only:text-[...]` dims the\n text without the disabled fill. It was styled but never demonstrated. */}\n<label className=\"flex flex-col gap-1 text-sm w-full max-w-xs\">\n Read-only\n <Textarea readOnly defaultValue=\"Submitted 14:02, no further edits\" />\n</label>"
2964
3105
  },
3106
+ {
3107
+ "caption": "Filled by AI, with a revert",
3108
+ "source": "const AI_NOTES = 'Ships with two midcaps and a sling. Ask for the 1.2 J spring if you play indoors.'\n\n<div className=\"flex flex-col gap-2 w-full max-w-sm\">\n <label htmlFor=\"ai-notes\" className=\"text-sm\">Order notes</label>\n <AiLabel explanation=\"Drafted from the replica's listing. Check the details before you send it.\" />\n <Textarea id=\"ai-notes\" aiValue={AI_NOTES} defaultValue={AI_NOTES} />\n</div>"
3109
+ },
2965
3110
  {
2966
3111
  "caption": "With a counter",
2967
3112
  "source": "function CounterExample() {\n const max = 140\n const [value, setValue] = useState('')\n return (\n <label className=\"flex flex-col gap-1 text-sm w-full max-w-xs\">\n Field day summary\n <Textarea\n value={value}\n onChange={e => setValue(e.target.value)}\n maxLength={max}\n aria-describedby=\"textarea-docs-counter\"\n />\n <span id=\"textarea-docs-counter\" className=\"text-xs text-[var(--color-text-secondary)]\">\n {value.length} / {max}\n </span>\n </label>\n )\n}\n\n<CounterExample />"
@@ -4045,6 +4190,12 @@
4045
4190
  "ChatMessage"
4046
4191
  ],
4047
4192
  "from": "src/components/molecules/chat-message/ChatMessage"
4193
+ },
4194
+ {
4195
+ "names": [
4196
+ "ResponseFeedback"
4197
+ ],
4198
+ "from": "src/components/molecules/chat-message/ResponseFeedback"
4048
4199
  }
4049
4200
  ],
4050
4201
  "requires": [
@@ -4086,6 +4237,21 @@
4086
4237
  "name": "className",
4087
4238
  "type": "string",
4088
4239
  "description": "Goes on the article."
4240
+ },
4241
+ {
4242
+ "name": "onSubmit (ResponseFeedback)",
4243
+ "type": "(vote: 'helpful' | 'not-helpful') => void | Promise<void>",
4244
+ "description": "Required. Runs on a thumbs up or down. Return a promise to show \"Submitting feedback\" until it settles; a rejection puts the buttons back."
4245
+ },
4246
+ {
4247
+ "name": "onDetail (ResponseFeedback)",
4248
+ "type": "(text: string) => void",
4249
+ "description": "Offers an optional, dismissable \"what was wrong\" box after a Not helpful vote. Leave it out for no such step."
4250
+ },
4251
+ {
4252
+ "name": "className (ResponseFeedback)",
4253
+ "type": "string",
4254
+ "description": "Goes on the group."
4089
4255
  }
4090
4256
  ],
4091
4257
  "api": [
@@ -4094,6 +4260,12 @@
4094
4260
  "kind": "type",
4095
4261
  "code": "export type ChatMessageRole = 'user' | 'assistant' | 'system'",
4096
4262
  "from": "src/components/molecules/chat-message/ChatMessage"
4263
+ },
4264
+ {
4265
+ "name": "FeedbackVote",
4266
+ "kind": "type",
4267
+ "code": "export type FeedbackVote = 'helpful' | 'not-helpful'",
4268
+ "from": "src/components/molecules/chat-message/ResponseFeedback"
4097
4269
  }
4098
4270
  ],
4099
4271
  "keyboard": [
@@ -4108,7 +4280,8 @@
4108
4280
  "Keep streaming true until the last piece of text has arrived, then set it to false once.",
4109
4281
  "Put tool calls and approvals in the assistant message where they happened, in order with its text.",
4110
4282
  "Use a system line for events in the conversation itself: stopped, retried, context cleared.",
4111
- "Give every message an author, including a system line (\"Status\"). It names the article."
4283
+ "Give every message an author, including a system line (\"Status\"). It names the article.",
4284
+ "Put ResponseFeedback in the actions of a generated reply once it has finished streaming, so a vote rates the whole reply."
4112
4285
  ],
4113
4286
  "donts": [
4114
4287
  "Don't animate the text in yourself. useStreamedText or your stream supplies it, and the thread handles announcing it.",
@@ -4145,7 +4318,7 @@
4145
4318
  },
4146
4319
  {
4147
4320
  "part": "Actions",
4148
- "description": "Small buttons under an assistant message: Copy, Retry."
4321
+ "description": "Small buttons under an assistant message: Copy, Retry, and ResponseFeedback (thumbs up and down)."
4149
4322
  },
4150
4323
  {
4151
4324
  "part": "System line",
@@ -4160,6 +4333,10 @@
4160
4333
  {
4161
4334
  "caption": "While the reply is still streaming in: aria-busy, an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers the message isn't finished, and a cursor that doesn't blink",
4162
4335
  "source": "<div className=\"flex w-full max-w-xl flex-col gap-6\">\n <ChatMessage role=\"assistant\" author=\"Assistant\" streaming>\n Depot 4 holds 1,240 replicas across 38 lines. The busiest\n </ChatMessage>\n</div>"
4336
+ },
4337
+ {
4338
+ "caption": "Feedback on a reply: thumbs up or down, one vote, and an optional detail step after Not helpful",
4339
+ "source": "const pretendSend = () => new Promise<void>(resolve => setTimeout(resolve, 800))\n\n<div className=\"flex w-full max-w-xl flex-col gap-6\">\n <ChatMessage\n role=\"assistant\"\n author=\"Assistant\"\n actions={<ResponseFeedback onSubmit={pretendSend} onDetail={() => {}} />}\n >\n Depot 4 holds 1,240 replicas. Eleven are below their reorder point.\n </ChatMessage>\n</div>"
4163
4340
  }
4164
4341
  ]
4165
4342
  },
@@ -4747,7 +4924,7 @@
4747
4924
  {
4748
4925
  "name": "children",
4749
4926
  "type": "ReactElement",
4750
- "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."
4751
4928
  },
4752
4929
  {
4753
4930
  "name": "className",
@@ -5655,6 +5832,12 @@
5655
5832
  "type": "readonly string[]",
5656
5833
  "description": "Ready-made prompts shown as chips. Clicking one submits it."
5657
5834
  },
5835
+ {
5836
+ "name": "editableSuggestions",
5837
+ "type": "boolean",
5838
+ "default": "false",
5839
+ "description": "Clicking a suggestion puts its text in the box to edit instead of sending it. Use it when a person will likely change the prompt first. The chips then stay usable while busy."
5840
+ },
5658
5841
  {
5659
5842
  "name": "placeholder",
5660
5843
  "type": "string",
@@ -5723,7 +5906,7 @@
5723
5906
  },
5724
5907
  {
5725
5908
  "part": "Suggestions",
5726
- "description": "A group of chips above the box. Clicking one submits its text. They're disabled while busy."
5909
+ "description": "A group of chips above the box. Clicking one submits its text (disabled while busy), or with editableSuggestions puts it in the box to edit."
5727
5910
  },
5728
5911
  {
5729
5912
  "part": "Box",
@@ -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
  {
@@ -740,11 +765,56 @@
740
765
  "light": "The dimmed leading-zero pad. 3.82:1 on surface.base, 22% of text.primary's 17.55:1 there, against the dark theme's 31%. Light theme needs a LIGHTER step than text.muted for the same role: muted aliases neutral.500 and measures 11.44:1 here, 65% of ink, which does not read as dimmed and out-shouts the signal.fill numeral it is padding (3.14:1 on this surface). neutral.400 would land at 6.58:1 and 38% of ink, closer to dark's share but more than twice the contrast of the digits beside it, so neutral.300 is the step that keeps the pad quieter than what it pads."
741
766
  }
742
767
  },
768
+ {
769
+ "name": "--color-ai-text",
770
+ "layer": "semantic",
771
+ "value": {
772
+ "dark": "#78b7ff",
773
+ "light": "#004593"
774
+ },
775
+ "use": "Text or an icon that marks something as made by AI: the AiLabel mark, the AI glyph in a field an AI filled. Always beside a word, never colour alone.",
776
+ "measured": {
777
+ "dark": "Measured by the gate: 9.55:1 on surface.base, 9.08:1 on surface.sunken, 8.32:1 on surface.raised. One step lighter than accent.info.text (info.500) so AI marks and status text do not read as the same thing.",
778
+ "light": "Measured by the gate: 8.39:1 on surface.base, 5.75:1 on surface.sunken, 8.92:1 on surface.raised. info.700 reached only 3.49:1 on surface.sunken, so the light theme takes info.800."
779
+ }
780
+ },
781
+ {
782
+ "name": "--color-ai-border",
783
+ "layer": "semantic",
784
+ "value": {
785
+ "dark": "#78b7ff",
786
+ "light": "#004593"
787
+ },
788
+ "use": "The boundary of a form field whose value an AI filled and the person has not changed. Input and Textarea paint it from aiValue; do not paint it by hand.",
789
+ "measured": {
790
+ "dark": "The edge of a field an AI filled. 7.91:1 on the AI-filled field (ai.wash over the control fill), far above the 3:1 SC 1.4.11 floor. Same stop as ai.text; a separate token because a boundary and text carry different floors.",
791
+ "light": "The edge of a field an AI filled. 5.24:1 on the AI-filled field (ai.wash over surface.sunken). Same stop as ai.text; a separate token because a boundary and text carry different floors."
792
+ }
793
+ },
794
+ {
795
+ "name": "--color-ai-wash",
796
+ "layer": "semantic",
797
+ "value": {
798
+ "dark": "rgba(90, 167, 255, 0.1)",
799
+ "light": "rgba(0, 99, 213, 0.08)"
800
+ },
801
+ "use": "The faint tint inside a field an AI filled, laid over the control fill. Input and Textarea paint it; nothing else should.",
802
+ "measured": {
803
+ "dark": "Tint laid over the control fill of an AI-filled field. Text on it is text.primary and gated (composited over surface.sunken). Never a fill on its own, never under coloured text other than ai.text.",
804
+ "light": "Tint laid over the control fill of an AI-filled field. Text on it is text.primary and gated (composited over surface.sunken)."
805
+ }
806
+ },
743
807
  {
744
808
  "name": "--color-focus-ring",
745
809
  "layer": "semantic",
746
- "value": "#f2550f",
747
- "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
+ }
748
818
  },
749
819
  {
750
820
  "name": "--color-chart-series-1",