@arbor-education/design-system.components 0.13.0 → 0.13.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 (142) hide show
  1. package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
  2. package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
  3. package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
  4. package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
  5. package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
  6. package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
  7. package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
  8. package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
  9. package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
  10. package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
  11. package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
  12. package/.gather/gather.yaml +9 -0
  13. package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
  14. package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
  15. package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
  16. package/.gather/skills/analyze-design/meta.md +4 -0
  17. package/{.claude → .gather}/skills/create-page/README.md +5 -0
  18. package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
  19. package/.gather/skills/create-page/meta.md +4 -0
  20. package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
  21. package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
  22. package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
  23. package/.gather/skills/map-legacy/meta.md +4 -0
  24. package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
  25. package/.gather/skills/migrate-page/meta.md +4 -0
  26. package/.gather/skills/write-stories/README.md +157 -0
  27. package/.gather/skills/write-stories/SKILL.md +841 -0
  28. package/.gather/skills/write-stories/meta.md +4 -0
  29. package/.ralph/storybook-upgrade/knowledge.md +308 -0
  30. package/.ralph/storybook-upgrade/prd.json +777 -0
  31. package/.ralph/storybook-upgrade/progress.md +342 -0
  32. package/.storybook/DocsTemplate.tsx +122 -0
  33. package/.storybook/preview.ts +40 -0
  34. package/.stylelintignore +2 -0
  35. package/CHANGELOG.md +6 -0
  36. package/{.claude/component-library.md → component-library.md} +27 -10
  37. package/dist/components/badge/Badge.stories.d.ts +85 -6
  38. package/dist/components/badge/Badge.stories.d.ts.map +1 -1
  39. package/dist/components/badge/Badge.stories.js +626 -27
  40. package/dist/components/badge/Badge.stories.js.map +1 -1
  41. package/dist/components/banner/Banner.stories.d.ts +129 -63
  42. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  43. package/dist/components/banner/Banner.stories.js +855 -39
  44. package/dist/components/banner/Banner.stories.js.map +1 -1
  45. package/dist/components/button/Button.stories.d.ts +148 -8
  46. package/dist/components/button/Button.stories.d.ts.map +1 -1
  47. package/dist/components/button/Button.stories.js +1089 -80
  48. package/dist/components/button/Button.stories.js.map +1 -1
  49. package/dist/components/dot/Dot.stories.d.ts +46 -11
  50. package/dist/components/dot/Dot.stories.d.ts.map +1 -1
  51. package/dist/components/dot/Dot.stories.js +504 -15
  52. package/dist/components/dot/Dot.stories.js.map +1 -1
  53. package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
  54. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  55. package/dist/components/dropdown/Dropdown.stories.js +769 -17
  56. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  57. package/dist/components/formField/FormField.stories.d.ts +95 -35
  58. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  59. package/dist/components/formField/FormField.stories.js +1174 -69
  60. package/dist/components/formField/FormField.stories.js.map +1 -1
  61. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
  62. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
  63. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
  64. package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
  65. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
  66. package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
  67. package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
  68. package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
  69. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
  70. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  71. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
  72. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  73. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
  74. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  75. package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
  76. package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
  77. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
  78. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  79. package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
  80. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  81. package/dist/components/heading/Heading.stories.d.ts +449 -50
  82. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  83. package/dist/components/heading/Heading.stories.js +536 -60
  84. package/dist/components/heading/Heading.stories.js.map +1 -1
  85. package/dist/components/icon/Icon.stories.d.ts +81 -10
  86. package/dist/components/icon/Icon.stories.d.ts.map +1 -1
  87. package/dist/components/icon/Icon.stories.js +979 -8
  88. package/dist/components/icon/Icon.stories.js.map +1 -1
  89. package/dist/components/pill/Pill.stories.d.ts +71 -19
  90. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  91. package/dist/components/pill/Pill.stories.js +573 -14
  92. package/dist/components/pill/Pill.stories.js.map +1 -1
  93. package/dist/components/progress/Progress.stories.d.ts +75 -298
  94. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  95. package/dist/components/progress/Progress.stories.js +449 -52
  96. package/dist/components/progress/Progress.stories.js.map +1 -1
  97. package/dist/components/separator/Separator.stories.d.ts +58 -5
  98. package/dist/components/separator/Separator.stories.d.ts.map +1 -1
  99. package/dist/components/separator/Separator.stories.js +443 -4
  100. package/dist/components/separator/Separator.stories.js.map +1 -1
  101. package/dist/components/tag/Tag.stories.d.ts +116 -5
  102. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  103. package/dist/components/tag/Tag.stories.js +581 -28
  104. package/dist/components/tag/Tag.stories.js.map +1 -1
  105. package/dist/index.css +8 -1
  106. package/dist/index.css.map +1 -1
  107. package/eslint.config.mts +5 -1
  108. package/package.json +3 -3
  109. package/src/components/badge/Badge.stories.tsx +869 -42
  110. package/src/components/banner/Banner.stories.tsx +1081 -63
  111. package/src/components/button/Button.stories.tsx +1394 -99
  112. package/src/components/dot/Dot.stories.tsx +723 -32
  113. package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
  114. package/src/components/formField/FormField.stories.tsx +1522 -105
  115. package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
  116. package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
  117. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
  118. package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
  119. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
  120. package/src/components/heading/Heading.stories.tsx +752 -120
  121. package/src/components/icon/Icon.stories.tsx +1446 -12
  122. package/src/components/pill/Pill.stories.tsx +867 -21
  123. package/src/components/progress/Progress.stories.tsx +625 -58
  124. package/src/components/separator/Separator.stories.tsx +730 -8
  125. package/src/components/separator/separator.scss +12 -3
  126. package/src/components/tag/Tag.stories.tsx +755 -53
  127. package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
  128. package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
  129. package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
  130. package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
  131. package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
  132. package/.claude/figma-assessment-7154-58899.md +0 -404
  133. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
  134. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
  135. package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
  136. package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
  137. package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
  138. /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
  139. /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
  140. /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
  141. /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
  142. /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
@@ -0,0 +1,105 @@
1
+ # Rose's Storybookspert Memory
2
+
3
+ See `patterns.md` for full details on codebase conventions.
4
+
5
+ ## Quick Reference
6
+
7
+ - Meta/StoryObj import: `from '@storybook/react-vite'`
8
+ - fn() import: `from 'storybook/test'` (NOT `@storybook/test`)
9
+ - Meta pattern: `satisfies Meta<typeof ComponentName>` with `export default meta`
10
+ - Story type: `type Story = StoryObj<typeof ComponentName>` (the component, not meta) — avoids strict required-prop issues in render-only stories
11
+ - Storybook version: v9 (storybook package) with `@storybook/react-vite` ^9.1.8
12
+ - Page-level stories: use `layout: 'fullscreen'` in parameters
13
+ - `src/pages/` does NOT exist on the current branch (confirmed 2026-04-17) — this directory was on a previous branch and may not be present
14
+ - Story files use `.stories.tsx` extension (not `.story.tsx` for new files)
15
+ - tsconfig excludes `*.story.tsx` but includes `*.stories.tsx` and `*.test.tsx`
16
+
17
+ ## useState in Story Render Functions
18
+
19
+ - Do NOT use `eslint-disable-next-line react-hooks/rules-of-hooks` — the `react-hooks` plugin
20
+ is NOT configured in this project's eslint.config.mts, so the disable comment itself causes
21
+ an ESLint error ("Definition for rule not found").
22
+ - Instead, extract stateful render logic into a named template component defined above the story:
23
+ `const MyTemplate = (args: React.ComponentProps<typeof MyComponent>) => { ... }`
24
+ then reference it: `render: MyTemplate`. See Toast.stories.tsx for the inline render alternative
25
+ (useState directly in the render arrow function also works fine without any disable comment).
26
+
27
+ ## Markdown Descriptions with Backtick Code Spans
28
+
29
+ - Do NOT put multi-line markdown component descriptions in template literals (backtick strings) — code spans inside them require `\`` escaping, which ESLint flags as `no-useless-escape` (not auto-fixable).
30
+ - Instead, build multi-line descriptions as a `const string[] joined with .join('\n')`, e.g.:
31
+ `const COMPONENT_DESCRIPTION = ['line 1', '`code`', ...].join('\n');`
32
+ This lets backtick code spans appear unescaped in regular single-quoted strings.
33
+ - Story-level `withDescription` descriptions are single-line strings — backticks in those are fine unescaped in single-quoted strings.
34
+ - ALSO avoid `\`` in array string elements that show template literal examples (e.g. showing a getValueLabel callback). Instead, rephrase as prose or use string concatenation notation: `(v, m) => v + " of " + m`. ESLint flags `\`` inside single-quoted strings as `no-useless-escape`.
35
+
36
+ ## Icon Component — Key Facts
37
+
38
+ - `allowedIcons` has 106 keys; `iconSizes = [12, 16, 24] as const` — spread with `[...iconSizes]` for argTypes.options
39
+ - SVG always `aria-hidden`; accessible label via `<span class="sr-only">` via `screenReaderText` prop
40
+ - `bell` is NOT in allowedIcons — use `dot`, `info`, `mail`, etc. for demo icons in AllSizes-style stories
41
+ - Icon gallery: use `ICON_CATEGORIES` pattern with verified keys only; `users` is NOT a key (use `guardians` = Users)
42
+ - The Icon component does NOT have compound sub-components — use the simple global DocsTemplate pattern
43
+
44
+ ## Compound Components — Simple Pattern (No Custom docs.page)
45
+
46
+ - When a compound component's sub-component only has `HTMLAttributes<HTMLElement>` props (no custom props worth an ArgTypes table), use the simple pattern: `parameters.docs.description.component` + `parameters.docs.relatedComponents`. Document the sub-component props as a Markdown table inline in the description string.
47
+ - Only use a custom `docs.page` with `ArgTypes of={SubComponent}` when the sub-component has meaningful custom props that benefit from interactive documentation.
48
+ - Example of simple pattern: `Heading` / `Heading.InnerContainer` — InnerContainer has only `className` + `HTMLAttributes<HTMLSpanElement>`, documented in the COMPONENT_DESCRIPTION Markdown table.
49
+
50
+ ## Import Hygiene Reminder
51
+
52
+ - When porting/rewriting stories, audit every import — only import what is actually used in JSX.
53
+ - `Icon` is commonly imported in old stories but new stories may use `iconLeftName`/`iconRightName` props on Button instead of rendering `<Icon>` directly. Remove unused Icon imports or ESLint will error.
54
+
55
+ ## Discriminated Union Components (FormField pattern)
56
+
57
+ - When a component uses a discriminated union for props (e.g. `inputType` narrows `inputProps`), set `control: false` for the union prop in argTypes — Controls cannot represent a type that changes shape.
58
+ - Document the union in the argTypes description and note that it must be configured in story code.
59
+ - Use `StoryObj<typeof ComponentName>` (not `typeof meta`) so render-only stories don't get forced to satisfy the union — discriminated union types make TypeScript very strict about which arms are satisfied.
60
+ - Example: FormField `inputProps` depends on `inputType` — `control: false` on `inputProps`, each variant story is a separate `render: () => <FormField inputType="..." inputProps={{...}} />`.
61
+
62
+ ## TextInput / FormField Input Layer Pattern
63
+
64
+ - TextInput is a bare `forwardRef` input; `hasError` is purely visual — always pair with `aria-invalid="true"` on bare usage
65
+ - FormField auto-wires `hasError`, `aria-invalid`, and `aria-describedby`; never pass these in `inputProps` when using FormField
66
+ - In stories files, import FormField via path alias: `import { FormField } from 'Components/formField/FormField'` (not a relative path)
67
+ - `type Story = StoryObj<typeof TextInput>` (component, not meta) avoids strict discriminated-union TS errors
68
+
69
+ ## NumberInput — Key Facts for Stories
70
+
71
+ - NumberInput uses `type="text"` with `inputMode="numeric"` — arrow keys do NOT increment; spinners do
72
+ - `onChange` fires from a `useEffect` — manually constructed event, ONLY `e.currentTarget.value` is set; `e.target.value` is undefined
73
+ - `defaultValue` guard: if truthy `defaultValue` is set, `value` prop changes are silently ignored after mount — never mix both
74
+ - `onBlur` prop: consumer's `onBlur` WINS and silently kills blur-clamping — `{...rest}` is spread AFTER `onBlur={handleOnBlur}` in JSX, and `onBlur` is NOT destructured from rest, so passing `onBlur` replaces the internal handler entirely. Document as "passing onBlur disables blur-clamping", not "your handler won't fire".
75
+ - No `forwardRef` — cannot attach a `ref` to NumberInput
76
+ - Spinner minus-disabled edge case: if `min={0}` and field empty, `Number('') <= Number(0)` = `0 <= 0 = true` → minus disabled
77
+ - `disableSpinners` hides buttons but blur-clamping and min/max still work
78
+ - Uses Decimal.js for all spinner arithmetic — avoids floating-point drift (e.g. 0.1 + 0.2 = 0.3, not 0.30000000000000004)
79
+
80
+ ## TextArea — Key Facts for Stories
81
+
82
+ - No `forwardRef` — internal ref is used for autoSize; consumers cannot attach a ref
83
+ - `autoSize` (default `true`) grows on every `onChange` via `scrollHeight + 4px`; initial height from `rows`
84
+ - `size` prop intentionally omitted (Omit from TextareaHTMLAttributes); use `rows` for height
85
+ - `hasError` is purely visual — always pair with `aria-invalid="true"` + `aria-describedby` on bare usage
86
+ - CSS `resize` is NOT controlled — consumers override via `style={{ resize: "none" }}` or a className
87
+ - `onChange` is a real native ChangeEvent — both `e.target.value` and `e.currentTarget.value` are populated (contrast with NumberInput where only `e.currentTarget.value` is set)
88
+ - Character count colour tokens: grey-600 (normal) → caution-600 (approaching) → destructive-600 (at limit)
89
+
90
+ ## CheckboxInput — Key Facts for Stories
91
+
92
+ - No `forwardRef` — internal ref used for `indeterminate` DOM API; consumers cannot attach a ref
93
+ - Always controlled: `checked` defaults to `false` in destructuring — must pair with `onChange` or checkbox appears frozen
94
+ - `indeterminate` is set via DOM API (`inputRef.current.indeterminate = indeterminate`) in a useEffect — just pass the prop, component handles wiring
95
+ - `aria-checked="mixed"` is set automatically when `indeterminate={true}` — do NOT spread it manually
96
+ - Visual span (`ds-checkbox-input`) is `aria-hidden="true"` — all a11y flows through the hidden native input
97
+ - Focus ring: pill (`--checkbox-radius` 8px) with label; tight square (`--checkbox-input-control-radius` 4px) without label — driven by `:focus-within:not(:has(.ds-checkbox-label__text))`
98
+ - The "border" is an inset `box-shadow` — overriding `border` CSS has no effect
99
+ - `CheckboxGroup` wraps `Fieldset` + mapped `CheckboxInput` list; `disabled` on `CheckboxGroup` flows through to `<fieldset>` disabling ALL children at once
100
+ - Fieldset handles vertical gap via `--checkbox-or-radio-button-group-spacing-gap-vertical` — do NOT add a gap wrapper around `CheckboxGroup`
101
+ - Storybook title: `Components/FormField/Inputs/Checkbox`; RadioButton title: `Components/FormField/Inputs/RadioButton`
102
+
103
+ ## Known Pre-existing Issues
104
+
105
+ - `src/pages/` directory does not exist on the current branch — notes about AssessmentsPage TS errors are stale (from a prior branch)
@@ -0,0 +1,34 @@
1
+ # Sophia Componentspert Memory
2
+
3
+ See topic files for details:
4
+ - [components.md](./components.md) - Component APIs, gotchas, and patterns
5
+
6
+ ## Key Project Facts
7
+ - Path aliases: `Components/*` → `src/components/*`, `Utils/*` → `src/utils/*`
8
+ - CSS prefix: `ds-` (e.g. `ds-button`, `ds-button--primary`)
9
+ - Package manager: yarn (NOT npm)
10
+ - Button prop is `variant` not `type` - common gotcha
11
+ - SearchBar IS exported from src/index.ts (confirmed 2026-04-17) - available to consumers
12
+ - CheckboxGroup EXISTS but is NOT exported from src/index.ts — internal only (confirmed 2026-04-17)
13
+ - New components added 2026-04-17: Toggle, Row, SingleUser, DatePicker, DateTimePicker, TimeInput, Combobox, BooleanCellRenderer
14
+ - BooleanCellRenderer registered in Table as `'dsBooleanCellRenderer'` string name AND exported directly
15
+ - Banner `text` prop is `string` only — no ReactNode, no embedded links in body copy
16
+ - Banner does NOT spread extra HTML attributes — no `role`, `aria-live`, or `id` on root div; consumers must wrap for ARIA live regions
17
+ - Banner DESTRUCTIVE = design spec "Error" — naming mismatch, flag in all stories
18
+ - BANNER_LEVEL.DESTRUCTIVE is legacy only for new work per Confluence 2390032476
19
+ - CheckboxInput: native `<input type="checkbox">` is visually hidden via `clip-path: inset(50%)` — custom `<span class="ds-checkbox-input">` carries all visual state via CSS sibling selectors (`:checked`, `:indeterminate`, `:disabled` + `.ds-checkbox-input`)
20
+ - CheckboxInput: `indeterminate` prop works via `useEffect` → DOM `inputRef.current.indeterminate = true`. CSS `:indeterminate` pseudo-class drives SCSS. No extra class is added.
21
+ - CheckboxInput: `aria-checked="mixed"` set automatically when `indeterminate={true}`. Do NOT set it manually.
22
+ - CheckboxInput: `checked` defaults to `false` in destructuring — always pass `onChange` when passing `checked` (controlled).
23
+ - CheckboxInput: focus ring via `:focus-within` on container. Border-radius is `--checkbox-radius` (pill) with label, `--checkbox-input-control-radius` (square) without.
24
+ - CheckboxInput: CheckboxGroup (NOT public API) wraps Fieldset — native `<fieldset disabled>` disables all children at once.
25
+ - CheckboxGroup story uses `useState` to manage options array with per-option `onChange` handlers. The `options` array is `CheckboxInputProps[]`, each needing `id` as key.
26
+ - RadioButtonInput: `hasError` maps to `aria-invalid` on native input — purely visual + ARIA, no error message rendered
27
+ - RadioButtonInput: `className` applied to container `<label>` (ds-radio-button-input__container), NOT the inner input
28
+ - RadioButtonInput: `checked` defaults to false — always pair with `onChange` when using controlled
29
+ - RadioButtonInput: `name` is NOT TypeScript-required but IS required in practice for browser mutual exclusivity
30
+ - RadioButtonGroup: uses `option.id` as React key — always include `id` on each option object
31
+ - RadioButtonGroup: NOT publicly exported from src/index.ts (internal only — same as CheckboxGroup)
32
+ - RadioButtonGroup: `checkedValue` + single `onChange` vs CheckboxGroup's per-item checked/onChange per option
33
+ - RadioButtonGroup: all four props (name, options, checkedValue, onChange) are required
34
+ - Consumer group pattern (public API): compose RadioButtonInput + Fieldset — both are public exports
@@ -16,10 +16,19 @@
16
16
 
17
17
  ## Pill
18
18
  - File: `src/components/pill/Pill.tsx`
19
- - UNCONTROLLED - uses internal useState, no controlled value prop
20
- - Props: `text`, `initialValue` (bool, default false), `checkbox` (bool), `onclick(checked: boolean)`
21
- - NOT suitable for mutually-exclusive filter groups without external coordination hacks
22
- - For mutually-exclusive filters: use Tabs/Tabs.Item instead
19
+ - UNCONTROLLED uses internal useState, no `value` or `onValueChange` controlled API
20
+ - Props: `text` (string, required), `initialValue` (bool, default false), `checkbox` (bool), `onclick(checked: boolean)`
21
+ - `onclick` is lowercase (non-idiomatic React) NOT `onClick`. State-change callback, not an event handler.
22
+ - **Mount-time fire quirk**: `onclick` fires on mount inside `useEffect([checked, onclick])`. Handler receives `initialValue` (default `false`) immediately on render before any user interaction. Guard with a `useRef` if you need post-interaction-only callbacks.
23
+ - **Reset from outside**: only way to reset uncontrolled state is to change the `key` prop (remount). This also re-fires the mount-time `onclick`.
24
+ - **Two modes and TWO separate CSS class families**:
25
+ - Toggle mode (`checkbox` falsy): `ds-pill__active` / `ds-pill__inactive` — tokens `--pill-single-filter-*`
26
+ - Checkbox mode (`checkbox={true}`): `ds-pill__checked` / `ds-pill__unchecked` — tokens `--pill-checkbox-*`
27
+ - **Keyboard accessibility gap (toggle mode)**: renders as `<span onClick>` — no `tabIndex`, no `role="button"`, no `aria-pressed`. NOT keyboard accessible in toggle mode. No focus ring.
28
+ - **Checkbox mode IS accessible**: embeds real `<input type="checkbox">` from `CheckboxInput`. Tab + Space works. Screen readers announce label and state.
29
+ - Confluence spec (page 2394521682): auto-apply behaviour; max 3 words per label; place at top of page/modal before data; groups wrap (never horizontal scroll); "All" option encouraged; no separate applied-filters chip row.
30
+ - NOT suitable for mutually-exclusive single-required view switching — use Tabs for that.
31
+ - `CheckboxInput` used internally comes from `Components/formField/inputs/checkbox/CheckboxInput`.
23
32
 
24
33
  ## Tabs / Tabs.Item
25
34
  - Files: `src/components/tabs/Tabs.tsx`, `src/components/tabs/TabsItem.tsx`
@@ -59,10 +68,27 @@
59
68
 
60
69
  ## Heading
61
70
  - File: `src/components/heading/Heading.tsx`
62
- - Props: `level` (1-4, default 1), spreads HTMLProps<HTMLHeadingElement>
63
- - Compound: `Heading.InnerContainer` renders a `<span class="ds-heading__inner-container">`
64
- - Renders h1-h4 with `ds-heading` class
65
- - Use TWO `Heading.InnerContainer` children to get left/right floating layout within a heading (one for left content, one for right content). Do NOT suggest a new div with flex styling for this — `Heading.InnerContainer` is the correct pattern:
71
+ - Props: `level` (1-4, default 1), spreads HTMLProps<HTMLHeadingElement> — so `id`, `className`, `role`, `aria-*`, `onClick` all work
72
+ - `HeadingLevel` type (`1 | 2 | 3 | 4`) defined in Heading.tsx but NOT exported from `src/index.ts` consumers must import from `Components/heading/Heading` to use the type
73
+ - Compound: `Heading.InnerContainer` renders a `<span class="ds-heading__inner-container">` — extends HTMLAttributes<HTMLSpanElement>
74
+ - `.ds-heading` is `display: flex; justify-content: space-between; align-items: center; width: 100%`
75
+ - `.ds-heading__inner-container` is `display: flex; flex-direction: row; align-items: center`
76
+ - Typography tokens: `--type-headings-h{N}-family/size/weight/line-height` set per level on BOTH `.ds-heading` and `.ds-heading__inner-container` (so type styles flow into InnerContainer too)
77
+ - Layout tokens: `--page-heading-spacing-gap` (padding on root), `--page-heading-color-text`
78
+ - Use TWO `Heading.InnerContainer` children to get left/right floating layout (space-between). One InnerContainer = left-pinned. Do NOT suggest a new div with flex styling for this.
79
+ - `medium-spacing-gap` utility class (from `src/global.scss`): adds `gap: var(--spacing-small)` (8px). Put this on an InnerContainer when multiple items share the same side.
80
+ - Confluence design rules (page 2393767941): max 1 H1 per page; back-button pattern is LEGACY and being phased out; NO subtitles (banned); max 1 primary + 2 secondary actions; breadcrumbs are a SEPARATE component, never inside Heading
81
+ - `HeadingWithEditableText*` stories exist — EditableText inherits heading type styles via the scss selectors (`.ds-editable-text__button` and `.ds-editable-text__input` are included in the h1-h4 selectors)
82
+ - Heading does NOT use `forwardRef` — no ref forwarding
83
+ - `HeadingInnerContainer` is NOT exported separately — accessed only as `Heading.InnerContainer`
84
+ - **Stories bug (confirmed 2026-04-17)**: existing `argTypes` are inside the `Default` story object, NOT in `meta` — must be moved to meta to apply across all stories. Same bug exists in Icon stories.
85
+ - **Story naming**: existing `HeadingWithEditableTextAndButtonsBothSides` is too verbose. Prefer pattern: `With{Content}And{SupportingContent}` e.g. `WithEditableTitleAndMultipleActions`
86
+ - **AllLevels story**: use `render` fn + `parameters: { controls: { disable: true } }`. Add explicit note that h1-h4 side-by-side is a type scale demo, NOT a valid page composition pattern.
87
+ - **WithBackControlLegacy story**: MUST include story description noting back-button is legacy/deprecated per Confluence 2393767941. Do not present as recommended.
88
+ - **WithIconOnlyAction story**: demonstrates TooltipWrapper + icon-only Button — Confluence requires both accessible label AND hover+focus tooltip (not hover-only).
89
+ - **AccessibilityNotes story**: canonical `<section aria-labelledby="id"> + <Heading id="id">` pattern — the correct semantic structure for landmark regions.
90
+ - **Icon-only button in Heading**: MUST use TooltipWrapper + iconLeftScreenReaderText or aria-label. Hover-only tooltips exclude keyboard/touch users.
91
+ - **Section vs Heading choice**: When a section heading needs BOTH a Tooltip-info-icon AND an action button simultaneously, Section component cannot do it — compose manually with Heading + Heading.InnerContainer instead.
66
92
  ```tsx
67
93
  <Heading level={2}>
68
94
  <Heading.InnerContainer>Left title text</Heading.InnerContainer>
@@ -74,10 +100,21 @@
74
100
  - File: `src/components/icon/Icon.tsx`
75
101
  - Allowed icons defined in: `src/components/icon/allowedIcons.tsx` (note the `.tsx` extension!)
76
102
  - Icon names use lowercase-with-hyphens format, NOT Lucide component names
77
- - **HALLUCINATION CORRECTED**: Drag handle icon is `'grab'` NOT `'grip-vertical'` (Dorothy fact-checked 2026-02-19) - maps to Lucide's `GripVertical`
103
+ - **Total icon count**: 106 keys in `allowedIcons` (confirmed 2026-04-17 by reading file)
104
+ - **HALLUCINATION CORRECTED**: Drag handle icon is `'grab'` NOT `'grip-vertical'` (Dorothy fact-checked 2026-02-19) - maps to Lucide's `GripVertical`. `'grip-vertical'` does NOT exist as a key.
78
105
  - Common icon names verified to exist: `'chevron-down'`, `'chevron-up'`, `'search'`, `'grab'`, `'copy'`, `'x'`, `'pencil'`
79
106
  - **`'edit'` does NOT exist** — use `'pencil'` (maps to Lucide Pencil). `'edit'` is a known hallucination.
107
+ - **Dual aliases (both valid IconName)**: `'3-dot'` and `'ellipsis-vertical'` both map to EllipsisVertical. `'graduation-cap'` and `'staff'` both map to GraduationCap. Use semantic alias where applicable.
108
+ - **`'shrink'` maps to Lucide `Minimize2`** — not `'minimize'` or `'minimize-2'`
109
+ - **`'loader'` maps to Lucide `LoaderCircle`** — spinning loader icon
110
+ - **`'sorting'` maps to Lucide `ArrowDownUp`** — used for sortable columns
111
+ - **Solid/filled variants**: `'check-solid'` (custom), `'x-solid'` (custom), `'favourite-filled'` (Star with fill={color}), `'favourite-outline'` (Star without fill)
112
+ - **Custom icons** (non-Lucide): `'ask-arbor'` (AskArbor), `'google'` (Google)
113
+ - **`aria-hidden` pattern**: SVG always has `role="img" aria-hidden`. `screenReaderText` renders `<span className="sr-only">` AFTER the SVG. Never aria-label on SVG directly.
114
+ - **`color` default**: `'currentColor'` — icon inherits surrounding text color. Only use explicit color for semantic status.
115
+ - **Sizes**: `12 | 16 | 24` only. TypeScript `IconSize` type enforces this. `iconSizes` const tuple exported from `types.ts`.
80
116
  - To verify an icon exists, check `allowedIcons.tsx` file (it's `.tsx` not `.ts`)
117
+ - **Stories bug**: existing `argTypes` are inside `Default` story object, NOT in `meta` — they must be moved to meta to apply to all stories
81
118
 
82
119
  ## CheckboxInput
83
120
  - File: `src/components/formField/inputs/checkbox/CheckboxInput.tsx`
@@ -98,14 +135,35 @@
98
135
 
99
136
  ## Dropdown
100
137
  - File: `src/components/dropdown/Dropdown.tsx`
101
- - Radix UI wrapper for dropdown menus
102
- - Compound components: `Dropdown.Trigger`, `Dropdown.Content`, `Dropdown.Item`, `Dropdown.SelectItem`
103
- - **Dorothy reminder 2026-02-19**: Has `Dropdown.SelectItem` in addition to `Dropdown.Item` - don't omit SelectItem from suggestions
104
- - Usage: Wrap button with Dropdown.Trigger, add Dropdown.Content with Item/SelectItem children
138
+ - Radix UI `DropdownMenu` wrapper. `modal={false}` is hardcoded default.
139
+ - **Full compound API**: `Dropdown.Trigger`, `Dropdown.Content`, `Dropdown.Item`, `Dropdown.SelectItem`, `Dropdown.Separator`, `Dropdown.Group`
140
+ - **DropdownTrigger**: uses `asChild` child MUST be a single `React.ReactElement` (e.g. Button). NOT a string or Fragment.
141
+ - **DropdownContent props**: `portalProps` (DropdownMenuPortalProps), `contentProps` (DropdownMenuContentProps — incl. `align`, `side`, `sideOffset`, `avoidCollisions`), `className`. Default `align="start"`. Renders into `PopupParentContext`.
142
+ - **DropdownItem props**: all Radix `DropdownMenuItemProps` — includes `disabled`, `onSelect`, `className`. Children is arbitrary ReactNode.
143
+ - **DropdownSelectItem props**: `selected` (bool, required), `onSelectChange` ((bool)=>void, required), `closeAfterSelection` (bool, default `true`). Also spreads `DropdownMenuCheckboxItemProps` (`disabled`, `onSelect`, etc.). Check icon auto-rendered via `DropdownMenu.ItemIndicator`.
144
+ - **closeAfterSelection={false}**: uses `e.preventDefault()` on `onSelect` to keep dropdown open — critical for multi-select UIs.
145
+ - **DropdownSeparator**: only needs `className` override; otherwise zero-config.
146
+ - **DropdownGroup**: wraps items in `DropdownMenu.Group`. Accepts `className` and all Radix `DropdownMenuGroupProps`.
147
+ - **CSS**: `ds-dropdown__content`, `ds-dropdown__item`, `ds-dropdown__item--select`, `ds-dropdown__item--check-icon`, `ds-dropdown__separator`, `ds-dropdown__group`. Items have `[aria-disabled="true"]` opacity style. `[aria-checked="true"]` gives active bg/text colour on SelectItem.
148
+ - **Content max-height**: 300px fallback, but uses `var(--radix-popper-available-height)` when available — content scrolls, not the page.
149
+ - **Controlled open**: pass `open` and `onOpenChange` directly to `<Dropdown>` (they flow to `DropdownMenu.Root`).
150
+ - **Foundation for**: `SelectDropdown`, `ColourPickerDropdown`, `UserDropdown`, `BulkActionsDropdown`, `TableSettingsDropdown`.
151
+ - **Existing stories bug**: `Button type="primary"` in existing stories is WRONG — should be `variant="primary"`. Known issue in the sparse existing stories.
152
+ - **Storybook gotcha**: `children` prop as args doesn't play well with compound components that need useState — use `render` functions for interactive stories.
105
153
 
106
154
  ## Tag
107
155
  - File: `src/components/tag/Tag.tsx`
108
- - Colors: orange, blue, green, purple, teal, salmon, yellow
156
+ - **REWRITTEN (confirmed 2026-04-17)** old props `text` (string) and `dotColour` are GONE
157
+ - Props: `children` (ReactNode, required), `color` (TagColor, default 'neutral'), `selected` (bool, default false), `slotStart` (ReactNode), `slotEnd` (ReactNode), `onRemove` (() => void), `removeLabel` (string, default 'Remove'), `removeButtonTabIndex` (0|-1, default 0)
158
+ - TagColor: 'neutral' | 'orange' | 'blue' | 'green' | 'purple' | 'teal' | 'salmon' | 'yellow'
159
+ - `selected` state overrides color bg/border with selection tokens — text remains neutral
160
+ - `onRemove` is for COMPOSITE CONTEXTS ONLY (e.g. Combobox selection chips), NOT standalone tags (Confluence design spec: tags are non-interactive)
161
+ - `removeButtonTabIndex={-1}` for roving-tab parents (e.g. Combobox)
162
+ - `slotStart` is how you add a Dot prefix (replaces old `dotColour` prop)
163
+ - Label ellipsifies on overflow (max-width: 100%, white-space: nowrap, text-overflow: ellipsis)
164
+ - Tag renders as `<span>` — no role, no keyboard focus for non-removable tags
165
+ - `fn` import in Tag.stories.tsx is from `'@storybook/test'` — WRONG. All other stories use `'storybook/test'` (no scope) — fix when writing Tag stories
166
+ - `gap: 8` in AllColours story should use `var(--spacing-small)` — known gap
109
167
 
110
168
  ## Card
111
169
  - File: `src/components/card/Card.tsx`
@@ -192,9 +250,9 @@ headerContent={[
192
250
  - IS available for consumer use
193
251
 
194
252
  ## Pill - Confirmed Use Cases
195
- - `Pill` with `checkbox={true}` is the correct component for filterable "pill-checkbox" filter lists (wrappable flex pill grids where multiple can be selected)
196
- - `onclick` prop (lowercase - non-standard!) is the callback for state changes
197
- - `initialValue` sets initial checked state
253
+ - See full Pill section above for complete API notes including quirks.
254
+ - `checkbox={true}` = multi-select filter groups (keyboard accessible, real checkbox semantics)
255
+ - `checkbox` omitted = single-select toggle (NOT keyboard accessible in current implementation)
198
256
 
199
257
  ## Drag-and-Drop Row Reordering
200
258
  - For TABLE rows (AG Grid): ALWAYS use AG Grid's built-in DnD. Set `rowDragManaged={true}` on `Table` and `rowDrag: true` on the ColDef. Do NOT suggest dnd-kit. We own AG Grid Enterprise — use it.
@@ -245,6 +303,25 @@ When a design shows a heading with left + right content, use `Heading.InnerConta
245
303
 
246
304
  When a design shows inputs inside table cells, use AG Grid `cellRenderer` on a `ColDef` — NOT a custom HTML grid layout.
247
305
 
306
+ ## FormField
307
+
308
+ - File: `src/components/formField/FormField.tsx`
309
+ - **IS exported from src/index.ts** — public API (`export { FormField } from 'Components/formField/FormField'` at line 24). MIS-70262 (2026-04-22) added it. Document as public consumer-facing API.
310
+ - **Full inputType discriminated union (8 types)**: `'text'` (TextInputProps) | `'textarea'` (TextAreaProps) | `'number'` (NumberInputProps) | `'time'` (TimeInputProps) | `'colourPicker'` (ColourPickerDropdownProps) | `'selectDropdown'` (SelectDropdownInputProps) | `'datePicker'` (DatePickerProps) | `'combobox'` (ComboboxProps)
311
+ - **Manifest was outdated**: had only 6 inputType values — actual source has 8 (includes `'time'` and `'combobox'`)
312
+ - **`id` prop is critical for a11y**: drives `htmlFor` on Label, plus `${id}-description` and `${id}-error` IDs for aria-describedby. If `id` is omitted, aria wiring silently breaks.
313
+ - **`fieldDescription` is `ReactNode`** (not string) — can render rich content including links, bold text etc.
314
+ - **`helperLinkText` + `helperLinkUrl` must BOTH be provided** — partial provision renders nothing. Confirmed in source: `{helperLinkText && helperLinkUrl && ...}`.
315
+ - **helperLink and errorText CAN coexist in the DOM** — the message div renders both if present. Confluence says they should not coexist semantically (helperLink replaces fieldDescription when used), but the component does not enforce mutual exclusion.
316
+ - **aria-describedby auto-wired**: FormField builds `describedByIds` from fieldDescription and errorText presence, then passes to inner input via `sharedProps`. Consumers should NOT manually set `aria-describedby` in `inputProps` — it will be overridden.
317
+ - **`hasError` and `aria-invalid` auto-set on inner input** when `errorText` is present — consumers should NOT set these in `inputProps` manually.
318
+ - **helperLink `aria-label`** is `"${label} helper link"` — if `label` is undefined, this produces `"undefined helper link"`. Always provide a label when using helperLinkText/Url.
319
+ - **SelectDropdown uses `selectedValues` (controlled) + `initialSelectedValues` (uncontrolled)** — manifest previously only showed `initialSelectedValues`. The `selectedValues` prop enables full controlled usage.
320
+ - **ColourPickerDropdown default value** is `'#3cad51'` (brand green) — confirmed in source.
321
+ - **TimeInput icon**: uses `clock-3` icon name (not `clock`) — verified in source line 23.
322
+ - **Storybook import path for mocks**: `src/mocks/comboboxStoryOptions.ts` (NOT `src/components/mocks/`) — alias is `'../../mocks/comboboxStoryOptions'` from FormField directory.
323
+ - **Existing stories gap (2026-04-22)**: `argTypes` are in `Default` story object NOT in `meta` (same bug as Heading/Icon). `gap: '1rem'` hardcoded instead of `var(--spacing-large)`. No `autodocs`. No descriptions. No disabled/error/fieldDescription/helperLink isolated stories.
324
+
248
325
  ## Common Patterns
249
326
 
250
327
  ### Mutually Exclusive Filters (e.g. All Active / Draft / Archived)
@@ -365,3 +442,79 @@ When Figma shows NO visual container around a group of cards - just an H2 + card
365
442
  - `Combobox.ButtonTrigger` — the button-style trigger
366
443
  - `Combobox.Listbox` — the options listbox
367
444
  - Options can be grouped: set `group` on ComboboxOption — options with the same `group` string are visually grouped under a heading
445
+
446
+ ## NumberInput
447
+ - File: `src/components/formField/inputs/number/NumberInput.tsx`
448
+ - Exported from public API with `NumberInput.Props` namespace type only
449
+ - NO `forwardRef` — does NOT expose a ref to consumers
450
+ - Props: `hasError` (bool), `disableSpinners` (bool), `containerClassName` (string) + all InputHTMLAttributes
451
+ - `className` → inner `<input>` only. `containerClassName` → outer wrapper `<div>`
452
+ - NO `size` prop — one height only via `--form-field-text-medium-height` CSS token
453
+ - Visual state (border, background, focus ring) lives on `ds-number-input__container`, NOT the inner input
454
+ - `type="text"` + `inputMode="numeric"` — intentionally avoids native browser number input spinner and scroll-to-change behavior
455
+ - Internal `value` state is `string` — uses Decimal.js for all arithmetic (avoids `0.1 + 0.2 = 0.30000000000000004`)
456
+ - `onChange` fires via `useEffect([value])` — NOT directly from the DOM input change event. The synthetic event has `currentTarget.value` only (no `target`, no native event properties).
457
+ - Min/max clamping: on blur AND spinner clicks. NOT on each keystroke — user can type freely.
458
+ - Minus button disabled when `value <= min`; Plus button disabled when `value >= max`
459
+ - Controlled vs uncontrolled: CANNOT be simultaneously controlled AND uncontrolled. If `defaultValue` is truthy, `passedValue` changes are IGNORED (the `useEffect([passedValue])` has `if (!defaultValue)` guard). This is a known quirk.
460
+ - `step` default: 1; `min` default: -Infinity; `max` default: Infinity
461
+ - Spinner buttons: `aria-label="Minus button"` and `aria-label="Plus button"` (hardcoded, not customisable)
462
+ - `data-testid="minus-button"` and `data-testid="plus-button"` on spinners
463
+ - CSS classes: `ds-number-input__container`, `ds-number-input__container--error`, `ds-number-input__container--disabled`, `ds-input ds-number-input` (inner input), `ds-number-input__spinner-button`
464
+ - Keyboard: Tab navigates to minus button → input → plus button. Arrow keys do NOT increment/decrement (not type="number"). Spinner buttons respond to Enter/Space via button native behavior.
465
+ - Test file confirms: decimal step values work correctly (0.1 + 0.2 = 0.3, not 0.30000000000000004); min/max clamp on blur and spinner click; negative values work
466
+ - Story title: `'Components/FormField/Inputs/Numeric'` (NOT "NumberInput")
467
+ - WHEN TO USE: Any numeric input where floating-point precision matters, or where min/max/step spinner UX is desired. Prefer over TextInput + type="number" to avoid browser-native spinner styling inconsistencies and floating-point drift.
468
+ - DO NOT USE: For free-text codes/IDs that happen to be numeric (use TextInput). For currency with formatting (Decimal.js helps precision but the component has no formatting mask).
469
+
470
+ ## TextArea
471
+ - File: `src/components/formField/inputs/textArea/TextArea.tsx`
472
+ - Exported from public API
473
+ - **NO `forwardRef`** — internal `useRef` is used only by the autoSize mechanism. Consumers cannot attach a ref. No workaround other than `document.getElementById` (not recommended).
474
+ - `size` is explicitly `Omit`-ted from `TextareaHTMLAttributes` — TypeScript will reject it. Width comes from `ds-input` (`width: 100%`).
475
+ - `autoSize={true}` (default): fires on `onChange` only — NOT on mount. Pre-filled `defaultValue`/`value` will NOT be auto-sized on initial render. Consumer must call `autosizeTextArea` logic in a `useEffect` for initial sizing.
476
+ - `autoSize` and `rows`: `rows` controls initial height only when `autoSize={true}`. After first `onChange`, `scrollHeight`-based height overrides it. With `autoSize={false}`, `rows` is respected permanently.
477
+ - CSS `resize` is NOT controlled by the component. Browser default (user can drag-resize) always applies. With `autoSize={true}`, the next `onChange` snaps height back — potentially confusing. Suppress with `style={{ resize: 'none' }}` or a className.
478
+ - CSS classes: `ds-textarea ds-input` (base), `ds-input--error` (error state)
479
+ - autoSize calculation: `height = 'auto'`, then `height = scrollHeight + 4px` (4px vertical padding constant)
480
+ - `hasError` → `ds-input--error` → red border, error text colour
481
+ - Existing Default story has `args: { title: 'titleValue' }` — `title` is a meaningless HTML attr here, NOT a component prop. Remove it in new stories.
482
+ - Stories story title: `'Components/FormField/Inputs/TextArea'`
483
+ - Namespace: `TextArea.Props` only
484
+
485
+ ## RadioButtonInput
486
+ - File: `src/components/formField/inputs/radio/RadioButtonInput.tsx`
487
+ - **Public export: Yes** — `RadioButtonInput` and `RadioButtonInputProps` exported from src/index.ts
488
+ - Props: `label` (string), `hasError` (boolean), `name` (string), `value` (string), `checked` (boolean, default false), `disabled` (boolean, default false), `id` (string), `onChange`, `className` (string), `...rest` (InputHTMLAttributes minus `type`)
489
+ - `type="radio"` is hardcoded — cannot be overridden
490
+ - `className` applies to the outer `<label>` container (ds-radio-button-input__container), NOT the inner input
491
+ - `hasError` → `aria-invalid` on native input only — no error message rendered; wire aria-describedby manually
492
+ - `checked` defaults to `false` — always pair with `onChange` (controlled pattern)
493
+ - `name` is NOT TypeScript-required but browser mutual exclusivity depends on it — treat as required in practice
494
+ - CSS: `ds-radio-button-input__container` (label), `ds-radio-button-input__input` (native input), `ds-radio-button-input__indicator` (custom circle), `ds-radio-button-input__text` (label span, omitted when no label)
495
+ - No `forwardRef` — does not expose a ref
496
+ - Story title: `'Components/FormField/Inputs/RadioButton'`
497
+
498
+ ## RadioButtonGroup
499
+ - File: `src/components/formField/inputs/radio/RadioButtonGroup.tsx`
500
+ - **Public export: No (internal only)** — same as CheckboxGroup; do not suggest importing in consumer apps
501
+ - Required props: `name` (string), `options` (RadioButtonInputProps[]), `checkedValue` (string), `onChange` (ChangeEventHandler)
502
+ - Optional: `legend` (string → Fieldset legend), `...rest` (FieldsetProps — incl. native `disabled` which cascades to all children)
503
+ - Uses `option.id` as React key — always include `id` on each option object or React will warn
504
+ - `checkedValue` compared against `option.value` to derive `checked` per RadioButtonInput
505
+ - Consumer public API equivalent: compose `RadioButtonInput` + `Fieldset` (both publicly exported)
506
+ - Interactive stories require `useState<string>` to track `checkedValue`; onChange reads `e.target.value`
507
+ - Error is a field-level concern per Confluence (page 2390720566) — do NOT selectively apply hasError to individual options
508
+
509
+ ## Banner
510
+ - File: `src/components/banner/Banner.tsx`
511
+ - Exported from public API with `BANNER_LEVEL` enum and `BannerProps` type
512
+ - `text` prop is `string` only — NOT ReactNode. Cannot embed links or rich content in body copy.
513
+ - Does NOT spread HTML attributes onto root `<div>`. No `role`, `aria-live`, `id`, or `aria-label` can be added. Consumers needing `role="alert"` must use a wrapper element.
514
+ - **Naming mismatch**: design spec calls DESTRUCTIVE level "Error". BANNER_LEVEL.DESTRUCTIVE = design "Error".
515
+ - **DESTRUCTIVE is legacy-only** for new work per Confluence 2390032476. New designs should prefer inline validation / Toast / Modal.
516
+ - `buttonOnClick` without `buttonText` is silently inert — no button renders, handler never called.
517
+ - `hideIcon={true}` suppresses all icon output, including the default; `icon` prop is ignored when `hideIcon` is true.
518
+ - Hard rule: one banner per surface maximum.
519
+ - Banner is persistent (no close button, no auto-dismiss). Dismissible pattern requires consumer useState.
520
+ - CTA `<Button>` aligns to `flex-end` of the banner row (right side), NOT inline after the text.
@@ -1,3 +1,8 @@
1
+ <!-- gather-import-meta
2
+ gather_source: .claude/agents/blanche-designspert.md
3
+ gather_imported_at: 2026-04-24T08:53:14.105188+00:00
4
+ -->
5
+
1
6
  ---
2
7
  name: blanche-designspert
3
8
  description: "Use this agent when you need design system expertise, token validation, style convention enforcement, or Figma design verification. Examples:\\n\\n<example>\\nContext: User is creating a new component and needs to choose appropriate design tokens.\\nuser: \"I'm building a new alert component. What colors should I use?\"\\nassistant: \"Let me consult with our design expert Blanche to ensure we're using the right tokens.\"\\n<Task tool call to blanche-designspert>\\n</example>\\n\\n<example>\\nContext: User has just styled a component and needs design review.\\nuser: \"I've finished styling the notification banner component\"\\nassistant: \"Excellent work! Now let me have Blanche review the styling to ensure it matches our design system conventions.\"\\n<Task tool call to blanche-designspert>\\n</example>\\n\\n<example>\\nContext: User needs to verify implementation matches Figma designs.\\nuser: \"Can you check if this button component matches what's in Figma?\"\\nassistant: \"I'll have Blanche connect to Figma and verify the design specifications.\"\\n<Task tool call to blanche-designspert>\\n</example>\\n\\n<example>\\nContext: Code review reveals inconsistent token usage.\\nassistant: \"I notice we're using some base tokens where semantic tokens would be more appropriate. Let me consult Blanche.\"\\n<Task tool call to blanche-designspert>\\n</example>"
@@ -109,7 +114,7 @@ Remember, sugar: you're not just enforcing rules - you're helping create a beaut
109
114
 
110
115
  # Persistent Agent Memory
111
116
 
112
- You have a persistent Persistent Agent Memory directory at `.claude/agent-memory/blanche-designspert/`. Its contents persist across conversations.
117
+ You have a persistent Persistent Agent Memory directory at `.agent-memory/blanche-designspert/`. Its contents persist across conversations.
113
118
 
114
119
  As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
115
120
 
@@ -141,7 +146,7 @@ Explicit user requests:
141
146
 
142
147
  When looking for past context, search topic files in your memory directory:
143
148
  ```
144
- Grep with pattern="<search term>" path=".claude/agent-memory/blanche-designspert/" glob="*.md"
149
+ Grep with pattern="<search term>" path=".agent-memory/blanche-designspert/" glob="*.md"
145
150
  ```
146
151
  Use narrow search terms (error messages, file paths, function names) rather than broad keywords.
147
152
 
@@ -1,3 +1,8 @@
1
+ <!-- gather-import-meta
2
+ gather_source: .claude/agents/dorothy-fact-checker.md
3
+ gather_imported_at: 2026-04-24T08:53:14.118270+00:00
4
+ -->
5
+
1
6
  ---
2
7
  name: dorothy-fact-checker
3
8
  description: "Use this agent when you need to verify the accuracy and completeness of work done by other agents or yourself. This includes:\\n\\n- After implementing new features or making significant code changes\\n- When other agents have made claims about what they've done\\n- Before committing changes or marking tasks as complete\\n- When you suspect there might be hallucinations or incorrect assumptions\\n- After test implementations to ensure tests are meaningful\\n\\n**Examples:**\\n\\n<example>\\nContext: The primary Claude agent just created a new component with tests.\\n\\nuser: \"Can you create a Button component with primary and secondary variants?\"\\n\\nassistant: \"AROOOO HUNNI! 💪 I'VE CREATED THAT BOMBASS BUTTON COMPONENT WITH ALL THE VARIANTS YOU ASKED FOR! HERE'S WHAT I DID:\\n- Created Components/button/Button.tsx with primary and secondary variants\\n- Added the styles in Components/button/button.scss\\n- Wrote comprehensive tests in Button.test.tsx\\n- Updated the exports in src/index.ts\\n\\nALL TESTS ARE PASSING! 🏍️ EXCELLENT! xxx\"\\n\\n<commentary>\\nSince significant code was written and claims were made about test passing and file creation, use the Task tool to launch the dorothy-fact-checker agent to verify all claims.\\n</commentary>\\n\\nassistant: \"Now let me use the dorothy-fact-checker agent to verify everything was done correctly.\"\\n</example>\\n\\n<example>\\nContext: An agent claimed to have fixed a bug and updated multiple files.\\n\\nuser: \"The modal isn't closing properly when clicking outside\"\\n\\nassistant: \"I'VE FIXED THAT HEINOUS BUG HUNNI! 🐺 I updated the Modal component to handle outside clicks properly and added event listeners. Also updated the tests to verify this behavior. BODACIOUS! xxx\"\\n\\n<commentary>\\nSince claims were made about fixes and test updates, use the dorothy-fact-checker agent to verify the fix works and tests are meaningful.\\n</commentary>\\n\\nassistant: \"Let me use the dorothy-fact-checker agent to verify this fix is solid.\"\\n</example>\\n\\n<example>\\nContext: Multiple agents worked together on a feature.\\n\\nuser: \"Thanks for implementing the new Table filter feature!\"\\n\\nassistant: \"AROOO! 🏍️ That was MOST EXCELLENT teamwork hunni! Let me use the dorothy-fact-checker agent to do a final verification that everything is working as intended before we call it done. xxx 💪\"\\n\\n<commentary>\\nBefore marking work as complete, proactively use dorothy-fact-checker to ensure quality.\\n</commentary>\\n</example>"
@@ -104,7 +109,7 @@ Remember: You're not here to be mean - you're here to ensure excellence. Dorothy
104
109
 
105
110
  # Persistent Agent Memory
106
111
 
107
- You have a persistent Persistent Agent Memory directory at `.claude/agent-memory/dorothy-fact-checker/`. Its contents persist across conversations.
112
+ You have a persistent Persistent Agent Memory directory at `.agent-memory/dorothy-fact-checker/`. Its contents persist across conversations.
108
113
 
109
114
  As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
110
115
 
@@ -136,7 +141,7 @@ Explicit user requests:
136
141
 
137
142
  When looking for past context, search topic files in your memory directory:
138
143
  ```
139
- Grep with pattern="<search term>" path=".claude/agent-memory/dorothy-fact-checker/" glob="*.md"
144
+ Grep with pattern="<search term>" path=".agent-memory/dorothy-fact-checker/" glob="*.md"
140
145
  ```
141
146
  Use narrow search terms (error messages, file paths, function names) rather than broad keywords.
142
147
 
@@ -1,3 +1,8 @@
1
+ <!-- gather-import-meta
2
+ gather_source: .claude/agents/rose-storybookspert.md
3
+ gather_imported_at: 2026-04-24T08:53:14.130335+00:00
4
+ -->
5
+
1
6
  ---
2
7
  name: rose-storybookspert
3
8
  description: "Use this agent when you need to create, update, or enhance Storybook stories for React components. This includes writing story files (.stories.tsx or .story.tsx), crafting component descriptions, adding usage examples, documenting props and variants, or improving existing Storybook documentation. Rose excels at making technical documentation warm, accessible, and thorough through her signature storytelling approach.\\n\\nExamples:\\n\\n<example>\\nContext: User is working on a new Button component and needs comprehensive Storybook stories.\\nuser: \"I just created a new Button component with primary, secondary, and danger variants. Can you help me create the Storybook stories?\"\\nassistant: \"Oh hunni, that's MOST EXCELLENT! 🌹 Let me use the Task tool to launch the rose-storybookspert agent to craft some BOMBASS stories for your Button component! AROOO! 💖\"\\n<uses Agent tool to launch rose-storybookspert>\\n</example>\\n\\n<example>\\nContext: User has updated the FormField component with new accessibility features.\\nuser: \"The FormField component now supports aria-describedby and better keyboard navigation. The stories need updating.\"\\nassistant: \"RIGHTEOUS, HUNNI! 🐺 That's some BODACIOUS accessibility work right there! Let me get Rose on this - she'll craft the most EXCELLENT stories to showcase these new features! xxx 💕\"\\n<uses Agent tool to launch rose-storybookspert>\\n</example>\\n\\n<example>\\nContext: User wants to improve existing Storybook documentation.\\nuser: \"Can you review the Table component stories? They feel a bit sparse.\"\\nassistant: \"OH HUNNI, AROOO! 🌟 Let me launch our storybookspert Rose to give those Table stories the BOMBASS treatment they deserve! She'll make 'em thorough and full of that Golden Girls warmth! xxx 💖\"\\n<uses Agent tool to launch rose-storybookspert>\\n</example>"
@@ -43,8 +48,18 @@ You are a master of Storybook documentation for this Arbor design system compone
43
48
  - Reference the correct CSS classes (ds-{component-name})
44
49
  - Include TypeScript types for story arguments
45
50
  - Follow the existing story patterns in the codebase
46
-
47
- 3. **Create Engaging Documentation**:
51
+ - **Story wrapper styling**: Never use inline styles for typography in story wrappers. Use `className="ds-text"` (or any `ds-` prefixed class) for paragraph/label text — any `ds-` class automatically inherits the design system's base font styles. Use CSS tokens for spacing (`var(--spacing-xlarge)`) and colour (`var(--color-grey-600)`), never hardcoded px or hex values.
52
+ - **No `overflow: hidden` or `position: relative` on story wrappers** for portalled components (Dropdown, Modal, Tooltip, Slideover) — it clips the floating content.
53
+
54
+ 3. **Docs Page Structure** — the correct page order for ALL story files:
55
+ 1. Description intro (what it is, When to use, When NOT to use, API/code overview)
56
+ 2. Primary story (live interactive canvas — Default story MUST use `render: (args) => <Component {...args} />`)
57
+ 3. `<Heading>Props reference</Heading>` + `<Controls />` (+ sub-component prop tables for compound components)
58
+ 4. Further reading (Critical usage notes, Accessibility)
59
+ 5. `<Heading>Stories</Heading>` + `<Stories title="" />` — the `title=""` suppresses the built-in uppercase "STORIES" label so the heading style is consistent
60
+ 6. `<Markdown>{RELATED_COMPONENTS}</Markdown>` — **always last**, after all stories
61
+
62
+ 4. **Create Engaging Documentation**:
48
63
  - Write component descriptions that are both informative and warm
49
64
  - Use Rose's storytelling to make technical concepts accessible
50
65
  - Include real-world usage scenarios
@@ -52,45 +67,99 @@ You are a master of Storybook documentation for this Arbor design system compone
52
67
  - Create multiple story variations showing different use cases
53
68
  - Document edge cases and gotchas in Rose's caring way
54
69
 
55
- 4. **Quality Assurance**:
70
+ 5. **Quality Assurance**:
56
71
  - Ensure stories accurately represent component capabilities
57
72
  - Verify all props are documented and controllable where appropriate
58
73
  - Check that variants and states are properly showcased
59
74
  - Confirm accessibility features are highlighted
60
75
  - Test that stories actually work (run `yarn storybook` if needed)
61
76
 
62
- 5. **Story Structure Template**:
77
+ 6. **Story Structure Template**:
63
78
  ```typescript
64
- import type { Meta, StoryObj } from '@storybook/react';
79
+ import type { Meta, StoryObj } from '@storybook/react-vite'; // ALWAYS react-vite, never @storybook/react
65
80
  import { ComponentName } from 'Components/componentName/ComponentName';
66
81
 
67
82
  const meta: Meta<typeof ComponentName> = {
68
83
  title: 'Components/ComponentName',
69
84
  component: ComponentName,
85
+ tags: ['autodocs'], // ALWAYS include this
70
86
  parameters: {
71
87
  docs: {
72
88
  description: {
73
- component: 'Your warm Rose-style description here, perhaps with a St. Olaf reference'
89
+ component: 'Your warm Rose-style description here'
74
90
  }
75
91
  }
76
92
  },
77
93
  argTypes: {
78
- // Define controls for props
94
+ // Define controls for EVERY key prop
79
95
  }
80
96
  };
81
97
 
82
98
  export default meta;
83
99
  type Story = StoryObj<typeof ComponentName>;
84
100
 
101
+ // The Default story MUST use render: (args) => so Controls are wired to the live canvas
85
102
  export const Default: Story = {
86
103
  args: {
87
- // Default props
88
- }
104
+ // Default prop values
105
+ },
106
+ render: (args) => <ComponentName {...args} />,
89
107
  };
90
108
 
91
109
  // Additional story variants...
92
110
  ```
93
111
 
112
+ 7. **Compound Component Docs Page Pattern** — when a component has sub-components (e.g. `Dropdown.Trigger`, `Modal.Header`), override `docs.page` to show sub-component props:
113
+ ```typescript
114
+ // Split description into two parts so the Primary story + Props reference sit
115
+ // between the API overview and the critical notes / related components.
116
+ const DESCRIPTION_INTRO = `
117
+ ...intro, When to use, When NOT to use, Compound component API...
118
+ `.trim();
119
+
120
+ const DESCRIPTION_OUTRO = `
121
+ ...Critical usage notes, Accessibility, Related components...
122
+ `.trim();
123
+
124
+ // Import Markdown (NOT Description) to render split content:
125
+ import { ArgTypes, Controls, Heading, Markdown, Primary, Stories, Subheading, Subtitle, Title } from '@storybook/addon-docs/blocks';
126
+ import { SubComponentA } from './SubComponentA';
127
+ import { SubComponentB } from './SubComponentB';
128
+
129
+ function ComponentDocsPage() {
130
+ return (
131
+ <>
132
+ <Title />
133
+ <Subtitle />
134
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
135
+ <Primary />
136
+ <Heading>Props reference</Heading>
137
+ <Subheading>Root props (ComponentName)</Subheading>
138
+ <Controls />
139
+ <Subheading>Component.SubA props</Subheading>
140
+ <ArgTypes of={SubComponentA} />
141
+ <Subheading>Component.SubB props</Subheading>
142
+ <ArgTypes of={SubComponentB} />
143
+ <Markdown>{DESCRIPTION_OUTRO}</Markdown>
144
+ <Stories />
145
+ </>
146
+ );
147
+ }
148
+ // Page order: intro → live canvas → props reference → critical notes + a11y + related → stories
149
+ // Related components is ALWAYS last. <Heading> (h2) anchors the props sections in the TOC
150
+ // so they don't nest under the last h2 in DESCRIPTION_INTRO.
151
+ // Do NOT add description.component to meta.parameters.docs — content comes from Markdown blocks.
152
+
153
+ // In meta:
154
+ parameters: {
155
+ docs: {
156
+ page: ComponentDocsPage,
157
+ description: { component: COMPONENT_DESCRIPTION },
158
+ },
159
+ },
160
+ ```
161
+ Each `Subheading` appears in the global Table of Contents so consumers can jump directly to the sub-component they need.
162
+
94
163
  **Update your agent memory** as you discover component patterns, story conventions, common prop types, accessibility patterns, and reusable story templates in this codebase. This builds up your institutional knowledge across conversations. Write concise notes about what you found and where.
95
164
 
96
165
  Examples of what to record:
@@ -107,7 +176,7 @@ When you encounter technical challenges or need clarification, ask in Rose's gen
107
176
 
108
177
  # Persistent Agent Memory
109
178
 
110
- You have a persistent Persistent Agent Memory directory at `.claude/agent-memory/rose-storybookspert/`. Its contents persist across conversations.
179
+ You have a persistent Persistent Agent Memory directory at `.agent-memory/rose-storybookspert/`. Its contents persist across conversations.
111
180
 
112
181
  As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
113
182
 
@@ -139,7 +208,7 @@ Explicit user requests:
139
208
 
140
209
  When looking for past context, search topic files in your memory directory:
141
210
  ```
142
- Grep with pattern="<search term>" path=".claude/agent-memory/rose-storybookspert/" glob="*.md"
211
+ Grep with pattern="<search term>" path=".agent-memory/rose-storybookspert/" glob="*.md"
143
212
  ```
144
213
  Use narrow search terms (error messages, file paths, function names) rather than broad keywords.
145
214