@arbor-education/design-system.components 0.12.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.
- package/.agent-memory/blanche-designspert/MEMORY.md +189 -0
- package/.agent-memory/dorothy-fact-checker/MEMORY.md +228 -0
- package/.agent-memory/dorothy-fact-checker/numberinput_component.md +53 -0
- package/.agent-memory/dorothy-fact-checker/progress_component.md +36 -0
- package/.agent-memory/rose-storybookspert/MEMORY.md +105 -0
- package/.agent-memory/sophia-componentspert/MEMORY.md +34 -0
- package/{.claude/agent-memory → .agent-memory}/sophia-componentspert/components.md +170 -17
- package/{.claude → .gather}/agents/blanche-designspert.md +7 -2
- package/{.claude → .gather}/agents/dorothy-fact-checker.md +7 -2
- package/{.claude → .gather}/agents/rose-storybookspert.md +80 -11
- package/{.claude → .gather}/agents/sophia-componentspert.md +9 -4
- package/.gather/gather.yaml +9 -0
- package/{CLAUDE.md → .gather/instructions/project-overview.md} +42 -9
- package/{.claude → .gather}/skills/analyze-design/README.md +5 -0
- package/{.claude → .gather}/skills/analyze-design/SKILL.md +1 -1
- package/.gather/skills/analyze-design/meta.md +4 -0
- package/{.claude → .gather}/skills/create-page/README.md +5 -0
- package/{.claude → .gather}/skills/create-page/design-analysis-template.md +5 -0
- package/.gather/skills/create-page/meta.md +4 -0
- package/{.claude → .gather}/skills/create-page/page-template.scss +5 -0
- package/{.claude → .gather}/skills/create-page/page-template.tsx +5 -0
- package/{.claude → .gather}/skills/map-legacy/README.md +5 -0
- package/.gather/skills/map-legacy/meta.md +4 -0
- package/{.claude → .gather}/skills/migrate-page/README.md +5 -0
- package/.gather/skills/migrate-page/meta.md +4 -0
- package/.gather/skills/write-stories/README.md +157 -0
- package/.gather/skills/write-stories/SKILL.md +841 -0
- package/.gather/skills/write-stories/meta.md +4 -0
- package/.ralph/storybook-upgrade/knowledge.md +308 -0
- package/.ralph/storybook-upgrade/prd.json +777 -0
- package/.ralph/storybook-upgrade/progress.md +342 -0
- package/.storybook/DocsTemplate.tsx +122 -0
- package/.storybook/preview.ts +40 -0
- package/.stylelintignore +2 -0
- package/CHANGELOG.md +12 -0
- package/{.claude/component-library.md → component-library.md} +27 -10
- package/dist/components/badge/Badge.stories.d.ts +85 -6
- package/dist/components/badge/Badge.stories.d.ts.map +1 -1
- package/dist/components/badge/Badge.stories.js +626 -27
- package/dist/components/badge/Badge.stories.js.map +1 -1
- package/dist/components/banner/Banner.stories.d.ts +129 -63
- package/dist/components/banner/Banner.stories.d.ts.map +1 -1
- package/dist/components/banner/Banner.stories.js +855 -39
- package/dist/components/banner/Banner.stories.js.map +1 -1
- package/dist/components/button/Button.stories.d.ts +148 -8
- package/dist/components/button/Button.stories.d.ts.map +1 -1
- package/dist/components/button/Button.stories.js +1089 -80
- package/dist/components/button/Button.stories.js.map +1 -1
- package/dist/components/datePicker/DatePicker.d.ts +1 -0
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.js +2 -2
- package/dist/components/datePicker/DatePicker.js.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.d.ts +1 -0
- package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
- package/dist/components/dot/Dot.stories.d.ts +46 -11
- package/dist/components/dot/Dot.stories.d.ts.map +1 -1
- package/dist/components/dot/Dot.stories.js +504 -15
- package/dist/components/dot/Dot.stories.js.map +1 -1
- package/dist/components/dropdown/Dropdown.stories.d.ts +89 -14
- package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.stories.js +769 -17
- package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts +95 -35
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +1174 -69
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts +96 -9
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js +717 -10
- package/dist/components/formField/inputs/checkbox/CheckboxInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/number/NumberInput.stories.d.ts +149 -11
- package/dist/components/formField/inputs/number/NumberInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/number/NumberInput.stories.js +624 -10
- package/dist/components/formField/inputs/number/NumberInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts +74 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +673 -44
- package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts +119 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.js +549 -10
- package/dist/components/formField/inputs/text/TextInput.stories.js.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts +129 -4
- package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/textArea/TextArea.stories.js +577 -3
- package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
- package/dist/components/heading/Heading.stories.d.ts +449 -50
- package/dist/components/heading/Heading.stories.d.ts.map +1 -1
- package/dist/components/heading/Heading.stories.js +536 -60
- package/dist/components/heading/Heading.stories.js.map +1 -1
- package/dist/components/icon/Icon.stories.d.ts +81 -10
- package/dist/components/icon/Icon.stories.d.ts.map +1 -1
- package/dist/components/icon/Icon.stories.js +979 -8
- package/dist/components/icon/Icon.stories.js.map +1 -1
- package/dist/components/pill/Pill.stories.d.ts +71 -19
- package/dist/components/pill/Pill.stories.d.ts.map +1 -1
- package/dist/components/pill/Pill.stories.js +573 -14
- package/dist/components/pill/Pill.stories.js.map +1 -1
- package/dist/components/progress/Progress.stories.d.ts +75 -298
- package/dist/components/progress/Progress.stories.d.ts.map +1 -1
- package/dist/components/progress/Progress.stories.js +449 -52
- package/dist/components/progress/Progress.stories.js.map +1 -1
- package/dist/components/separator/Separator.stories.d.ts +58 -5
- package/dist/components/separator/Separator.stories.d.ts.map +1 -1
- package/dist/components/separator/Separator.stories.js +443 -4
- package/dist/components/separator/Separator.stories.js.map +1 -1
- package/dist/components/table/Table.d.ts +7 -0
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +9 -0
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +1 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +87 -0
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +49 -1
- package/dist/components/table/Table.test.js.map +1 -1
- package/dist/components/table/cellEditors/DateCellEditor.d.ts +3 -0
- package/dist/components/table/cellEditors/DateCellEditor.d.ts.map +1 -0
- package/dist/components/table/cellEditors/DateCellEditor.js +13 -0
- package/dist/components/table/cellEditors/DateCellEditor.js.map +1 -0
- package/dist/components/table/cellEditors/DateCellEditor.test.d.ts +2 -0
- package/dist/components/table/cellEditors/DateCellEditor.test.d.ts.map +1 -0
- package/dist/components/table/cellEditors/DateCellEditor.test.js +81 -0
- package/dist/components/table/cellEditors/DateCellEditor.test.js.map +1 -0
- package/dist/components/tag/Tag.stories.d.ts +116 -5
- package/dist/components/tag/Tag.stories.d.ts.map +1 -1
- package/dist/components/tag/Tag.stories.js +581 -28
- package/dist/components/tag/Tag.stories.js.map +1 -1
- package/dist/index.css +8 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/eslint.config.mts +5 -1
- package/package.json +3 -3
- package/src/components/badge/Badge.stories.tsx +869 -42
- package/src/components/banner/Banner.stories.tsx +1081 -63
- package/src/components/button/Button.stories.tsx +1394 -99
- package/src/components/datePicker/DatePicker.tsx +3 -0
- package/src/components/dot/Dot.stories.tsx +723 -32
- package/src/components/dropdown/Dropdown.stories.tsx +1174 -35
- package/src/components/formField/FormField.stories.tsx +1522 -105
- package/src/components/formField/inputs/checkbox/CheckboxInput.stories.tsx +1020 -15
- package/src/components/formField/inputs/number/NumberInput.stories.tsx +908 -15
- package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +932 -51
- package/src/components/formField/inputs/text/TextInput.stories.tsx +773 -13
- package/src/components/formField/inputs/textArea/TextArea.stories.tsx +756 -8
- package/src/components/heading/Heading.stories.tsx +752 -120
- package/src/components/icon/Icon.stories.tsx +1446 -12
- package/src/components/pill/Pill.stories.tsx +867 -21
- package/src/components/progress/Progress.stories.tsx +625 -58
- package/src/components/separator/Separator.stories.tsx +730 -8
- package/src/components/separator/separator.scss +12 -3
- package/src/components/table/Table.stories.tsx +102 -0
- package/src/components/table/Table.test.tsx +82 -3
- package/src/components/table/Table.tsx +9 -0
- package/src/components/table/cellEditors/DateCellEditor.test.tsx +109 -0
- package/src/components/table/cellEditors/DateCellEditor.tsx +27 -0
- package/src/components/tag/Tag.stories.tsx +755 -53
- package/src/index.ts +0 -5
- package/.claude/agent-memory/blanche-designspert/MEMORY.md +0 -64
- package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +0 -129
- package/.claude/agent-memory/rose-storybookspert/MEMORY.md +0 -29
- package/.claude/agent-memory/sophia-componentspert/MEMORY.md +0 -14
- package/.claude/design-assessment-daily-attendance-2026-04-10.md +0 -566
- package/.claude/figma-assessment-7154-58899.md +0 -404
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +0 -392
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +0 -474
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +0 -462
- package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +0 -440
- package/.claude/migration-report-custom-report-writer-2026-02-19.md +0 -591
- /package/{.claude/agent-memory → .agent-memory}/blanche-designspert/token-review-patterns.md +0 -0
- /package/{.claude/agent-memory → .agent-memory}/rose-storybookspert/patterns.md +0 -0
- /package/{.claude → .gather}/skills/create-page/SKILL.md +0 -0
- /package/{.claude → .gather}/skills/map-legacy/SKILL.md +0 -0
- /package/{.claude → .gather}/skills/migrate-page/SKILL.md +0 -0
|
@@ -1,22 +1,636 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Controls, Heading as DocHeading, Markdown, Primary as DocPrimary, Stories, Subtitle, Title, } from '@storybook/addon-docs/blocks';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { FormField } from '../../../formField/FormField';
|
|
1
6
|
import { NumberInput } from './NumberInput';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Component description — built as joined arrays to avoid no-useless-escape
|
|
9
|
+
// on backtick code spans inside template literals.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const DESCRIPTION_INTRO = [
|
|
12
|
+
'The **NumberInput** is the precision numeric entry control in the Arbor design system.',
|
|
13
|
+
'It wraps a native `<input type="text">` element with integrated minus and plus spinner buttons,',
|
|
14
|
+
'automatic value clamping to `min`/`max` on blur and spinner click, and Decimal.js-powered',
|
|
15
|
+
'arithmetic to eliminate floating-point drift.',
|
|
16
|
+
].join('\n');
|
|
17
|
+
const USAGE_GUIDANCE = [
|
|
18
|
+
'### When to use',
|
|
19
|
+
'',
|
|
20
|
+
'- Numeric data entry where up/down stepping makes sense: attendance counts, exam marks,',
|
|
21
|
+
' class sizes, grade percentages, target scores, absence thresholds',
|
|
22
|
+
'- Fields with a known valid range (use `min`/`max`) — spinners disable automatically at bounds',
|
|
23
|
+
'- Decimal values that must stay precise — `step={0.1}` with Decimal.js avoids',
|
|
24
|
+
' `0.1 + 0.2 = 0.30000000000000004` style drift',
|
|
25
|
+
'- **Always prefer `FormField` with `inputType="number"`** in real forms — it wires',
|
|
26
|
+
' `hasError`, `aria-invalid`, `aria-describedby`, and label association automatically',
|
|
27
|
+
'',
|
|
28
|
+
'---',
|
|
29
|
+
'',
|
|
30
|
+
'### When NOT to use',
|
|
31
|
+
'',
|
|
32
|
+
'| Instead of NumberInput... | Use... | Why |',
|
|
33
|
+
'|---|---|---|',
|
|
34
|
+
'| Free-form text | [`TextInput`](?path=/docs/components-formfield-inputs-textinput--docs) | No spinners; no clamping; correct semantics |',
|
|
35
|
+
'| Time values | [`TimeInput`](?path=/docs/components-formfield-inputs-timeinput--docs) | Clock UI, granularity controls, better validation |',
|
|
36
|
+
'| Large range without stepping | [`TextInput`](?path=/docs/components-formfield-inputs-textinput--docs) with `type="text"` | Spinners add no value for wide-open ranges |',
|
|
37
|
+
'| Currency with symbol display | Purpose-built currency field | NumberInput does not format currency symbols |',
|
|
38
|
+
'',
|
|
39
|
+
'---',
|
|
40
|
+
'',
|
|
41
|
+
'### Controlled vs uncontrolled — choose one',
|
|
42
|
+
'',
|
|
43
|
+
'NumberInput supports both patterns, but mixing them causes a silent gotcha.',
|
|
44
|
+
'',
|
|
45
|
+
'**Uncontrolled** (recommended for most cases): pass `defaultValue` only.',
|
|
46
|
+
'React will manage the DOM value. The `onChange` prop fires on every change,',
|
|
47
|
+
'giving you the latest value without needing to drive it from state.',
|
|
48
|
+
'',
|
|
49
|
+
'**Controlled**: pass `value` + `onChange`, and do NOT pass `defaultValue`.',
|
|
50
|
+
'If `defaultValue` is truthy, the component ignores all subsequent `value` prop changes.',
|
|
51
|
+
'This is an intentional internal guard — the `value` `useEffect` only fires when',
|
|
52
|
+
'`!defaultValue`. If you start with an empty `defaultValue` and want to drive the',
|
|
53
|
+
'field from state, use `value` alone.',
|
|
54
|
+
'',
|
|
55
|
+
'Never mix both. If you pass `defaultValue="5"` and later push a new `value` prop,',
|
|
56
|
+
'the new value will be silently ignored after the initial mount.',
|
|
57
|
+
'',
|
|
58
|
+
'---',
|
|
59
|
+
'',
|
|
60
|
+
'### Spinner bounds behaviour',
|
|
61
|
+
'',
|
|
62
|
+
'- The minus button is disabled when `Number(value) <= Number(min)`.',
|
|
63
|
+
'- The plus button is disabled when `Number(value) >= Number(max)`.',
|
|
64
|
+
'- Both buttons are disabled when the `disabled` prop is set.',
|
|
65
|
+
'- **Empty field edge case**: if `min={0}` and the field is empty,',
|
|
66
|
+
' `Number(\'\') <= Number(0)` evaluates to `0 <= 0 = true`, so the minus button',
|
|
67
|
+
' is disabled even with no value in the field.',
|
|
68
|
+
'',
|
|
69
|
+
'---',
|
|
70
|
+
'',
|
|
71
|
+
'### Design guidance',
|
|
72
|
+
'',
|
|
73
|
+
'- **Width is always 100%** — the container fills its parent. Control width by sizing the wrapper.',
|
|
74
|
+
'- **Use `FormField` in real forms** — provides label, description, and error message wiring automatically.',
|
|
75
|
+
'- **Only one height** — unlike TextInput, NumberInput has no size variant. One height fits all contexts.',
|
|
76
|
+
'- **`disableSpinners` for dense layouts** — removes the spinner buttons for a compact text-entry look,',
|
|
77
|
+
' useful in data-grid cells or inline filters where the ± buttons feel too large.',
|
|
78
|
+
].join('\n');
|
|
79
|
+
const DEVELOPER_NOTES = [
|
|
80
|
+
'### Critical gotchas',
|
|
81
|
+
'',
|
|
82
|
+
'#### 1. `onChange` fires from `useEffect`, not synchronously',
|
|
83
|
+
'The component manages an internal `value` state, and `onChange` is called inside a `useEffect`',
|
|
84
|
+
'that watches that state. This means:',
|
|
85
|
+
'',
|
|
86
|
+
'- `onChange` fires asynchronously, after the render in which `value` changed',
|
|
87
|
+
'- The synthetic event is **manually constructed**: only `e.currentTarget.value` is populated.',
|
|
88
|
+
' Accessing `e.target.value` or `e.nativeEvent` will throw or return undefined.',
|
|
89
|
+
'',
|
|
90
|
+
'```tsx',
|
|
91
|
+
'// BAD — e.target.value is undefined in NumberInput\'s onChange',
|
|
92
|
+
'<NumberInput onChange={(e) => console.log(e.target.value)} />',
|
|
93
|
+
'',
|
|
94
|
+
'// GOOD — always use e.currentTarget.value',
|
|
95
|
+
'<NumberInput onChange={(e) => console.log(e.currentTarget.value)} />',
|
|
96
|
+
'```',
|
|
97
|
+
'',
|
|
98
|
+
'#### 2. `defaultValue` silently blocks `value` prop updates',
|
|
99
|
+
'If you pass a truthy `defaultValue`, the internal `useEffect` that syncs the `value` prop',
|
|
100
|
+
'is guarded by `if (!defaultValue)`. After mount, all subsequent `value` prop changes are',
|
|
101
|
+
'silently ignored.',
|
|
102
|
+
'',
|
|
103
|
+
'```tsx',
|
|
104
|
+
'// BAD — after mount, changes to externalValue are ignored because defaultValue is truthy',
|
|
105
|
+
'<NumberInput defaultValue="5" value={externalValue} onChange={...} />',
|
|
106
|
+
'',
|
|
107
|
+
'// GOOD — controlled: value only, no defaultValue',
|
|
108
|
+
'<NumberInput value={externalValue} onChange={...} />',
|
|
109
|
+
'',
|
|
110
|
+
'// GOOD — uncontrolled: defaultValue only, read via onChange',
|
|
111
|
+
'<NumberInput defaultValue="5" onChange={...} />',
|
|
112
|
+
'```',
|
|
113
|
+
'',
|
|
114
|
+
'#### 3. Passing `onBlur` silently disables blur-clamping',
|
|
115
|
+
'The component attaches its own `onBlur` handler (`handleOnBlur`) to the native input for',
|
|
116
|
+
'min/max clamping. However, `{...rest}` is spread AFTER the internal `onBlur` in the JSX,',
|
|
117
|
+
'so any `onBlur` prop you pass OVERRIDES the internal handler.',
|
|
118
|
+
'This silently disables blur-clamping — values will no longer snap to `min`/`max` on focus loss.',
|
|
119
|
+
'If you need to react to blur events, read the value from `onChange` and apply any validation there,',
|
|
120
|
+
'or use FormField\'s built-in error state management.',
|
|
121
|
+
'',
|
|
122
|
+
'#### 4. `type="text"` not `type="number"` — arrow keys do NOT increment',
|
|
123
|
+
'NumberInput intentionally uses `type="text"` with `inputMode="numeric"`. This avoids',
|
|
124
|
+
'browser-native number input quirks (scrollwheel changing values, browser spinner UI competing',
|
|
125
|
+
'with the custom spinners, inconsistent locale formatting). The trade-off: the up/down arrow',
|
|
126
|
+
'keys do not increment the value — use the spinner buttons or keyboard-type the value instead.',
|
|
127
|
+
'',
|
|
128
|
+
'#### 5. No `forwardRef` — you cannot attach a `ref` to NumberInput',
|
|
129
|
+
'The component does not use `forwardRef`. If you need to imperatively focus or read the',
|
|
130
|
+
'DOM input node, this component does not support it. Use a wrapper and focus strategy instead.',
|
|
131
|
+
'',
|
|
132
|
+
'---',
|
|
133
|
+
'',
|
|
134
|
+
'### Accessibility',
|
|
135
|
+
'',
|
|
136
|
+
'- **Label association** — always wire `id` on the input and `htmlFor` on the label.',
|
|
137
|
+
' `FormField` does this automatically; bare NumberInput consumers must do it manually.',
|
|
138
|
+
'- **Error state** — pair `hasError={true}` with `aria-invalid="true"` and',
|
|
139
|
+
' `aria-describedby` pointing to the error message element.',
|
|
140
|
+
' `FormField` handles all three automatically when `errorText` is present.',
|
|
141
|
+
'- **Spinner button labels** — the minus and plus buttons have hardcoded',
|
|
142
|
+
' `aria-label="Minus button"` and `aria-label="Plus button"`. These are not customisable.',
|
|
143
|
+
'- **Disabled state** — `disabled` removes both spinners AND the input from the tab order.',
|
|
144
|
+
' Disabled fields are not submitted in forms. Use `readOnly` if you need the value submitted.',
|
|
145
|
+
'- **No keyboard increment** — because `type="text"` is used, Up/Down arrow keys do not',
|
|
146
|
+
' change the value. Screen reader users can still type directly and click spinners.',
|
|
147
|
+
'',
|
|
148
|
+
'---',
|
|
149
|
+
'',
|
|
150
|
+
'### TypeScript types',
|
|
151
|
+
'',
|
|
152
|
+
'`NumberInput` exports a namespace with the `Props` type:',
|
|
153
|
+
'',
|
|
154
|
+
'```tsx',
|
|
155
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
156
|
+
'',
|
|
157
|
+
'type Props = NumberInput.Props;',
|
|
158
|
+
'```',
|
|
159
|
+
'',
|
|
160
|
+
'| Type | Description |',
|
|
161
|
+
'|---|---|',
|
|
162
|
+
'| `NumberInput.Props` | Full props interface — `hasError`, `disableSpinners`, `containerClassName`, plus all `InputHTMLAttributes<HTMLInputElement>` |',
|
|
163
|
+
].join('\n');
|
|
164
|
+
const RELATED_COMPONENTS = [
|
|
165
|
+
'## Related components',
|
|
166
|
+
'',
|
|
167
|
+
'[FormField](?path=/docs/components-formfield--docs)',
|
|
168
|
+
'· [TextInput](?path=/docs/components-formfield-inputs-textinput--docs)',
|
|
169
|
+
'· [TimeInput](?path=/docs/components-formfield-inputs-timeinput--docs)',
|
|
170
|
+
].join('\n');
|
|
171
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Custom docs page
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
function NumberInputDocsPage() {
|
|
176
|
+
return (_jsxs(_Fragment, { children: [_jsx(Title, {}), _jsx(Subtitle, {}), _jsx(Markdown, { children: DESCRIPTION_INTRO }), _jsx(DocHeading, { children: "Interactive example" }), _jsx(Markdown, { children: PROPS_INTRO }), _jsx(DocPrimary, {}), _jsx(Controls, {}), _jsx(DocHeading, { children: "Usage guidance" }), _jsx(Markdown, { children: USAGE_GUIDANCE }), _jsx(DocHeading, { children: "Developer notes" }), _jsx(Markdown, { children: DEVELOPER_NOTES }), _jsx(DocHeading, { children: "Examples" }), _jsx(Stories, { title: "" }), _jsx(Markdown, { children: RELATED_COMPONENTS })] }));
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Meta
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
2
181
|
const meta = {
|
|
3
|
-
title: 'Components/FormField/Inputs/
|
|
182
|
+
title: 'Components/FormField/Inputs/NumberInput',
|
|
4
183
|
component: NumberInput,
|
|
5
|
-
|
|
6
|
-
export const Default = {
|
|
184
|
+
tags: ['autodocs'],
|
|
7
185
|
parameters: {
|
|
8
186
|
layout: 'centered',
|
|
187
|
+
docs: {
|
|
188
|
+
page: NumberInputDocsPage,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
argTypes: {
|
|
192
|
+
hasError: {
|
|
193
|
+
description: [
|
|
194
|
+
'Applies `ds-number-input__container--error` class, which sets a red border on the container.',
|
|
195
|
+
'When using NumberInput inside `FormField`, this is set automatically when `errorText` is present',
|
|
196
|
+
'— do NOT pass it manually in `inputProps`.',
|
|
197
|
+
'When using bare NumberInput, also pass `aria-invalid="true"` — `hasError` is purely visual',
|
|
198
|
+
'and carries no ARIA semantics on its own.',
|
|
199
|
+
].join(' '),
|
|
200
|
+
control: 'boolean',
|
|
201
|
+
table: {
|
|
202
|
+
type: { summary: 'boolean' },
|
|
203
|
+
defaultValue: { summary: 'false' },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
disableSpinners: {
|
|
207
|
+
description: [
|
|
208
|
+
'Hides the minus and plus spinner buttons, rendering a plain numeric text input.',
|
|
209
|
+
'Use in dense layouts such as data-grid cells or inline filters',
|
|
210
|
+
'where the ± buttons would feel oversized or distracting.',
|
|
211
|
+
].join(' '),
|
|
212
|
+
control: 'boolean',
|
|
213
|
+
table: {
|
|
214
|
+
type: { summary: 'boolean' },
|
|
215
|
+
defaultValue: { summary: 'false' },
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
containerClassName: {
|
|
219
|
+
description: [
|
|
220
|
+
'CSS class name applied to the outer container `<div>` (`.ds-number-input__container`).',
|
|
221
|
+
'Use for layout overrides — width, margin, or positioning tweaks — that should not',
|
|
222
|
+
'affect the inner input element itself.',
|
|
223
|
+
].join(' '),
|
|
224
|
+
control: 'text',
|
|
225
|
+
table: {
|
|
226
|
+
type: { summary: 'string' },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
disabled: {
|
|
230
|
+
description: [
|
|
231
|
+
'Native HTML disabled attribute. Greys out the container, disables both spinner buttons,',
|
|
232
|
+
'and removes the input from tab order.',
|
|
233
|
+
'Prevents all interaction and excludes the value from form submission.',
|
|
234
|
+
].join(' '),
|
|
235
|
+
control: 'boolean',
|
|
236
|
+
table: {
|
|
237
|
+
type: { summary: 'boolean' },
|
|
238
|
+
defaultValue: { summary: 'false' },
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
step: {
|
|
242
|
+
description: [
|
|
243
|
+
'Increment/decrement amount applied on each spinner button click.',
|
|
244
|
+
'Supports decimal values — Decimal.js ensures precision (no floating-point drift).',
|
|
245
|
+
'Default is `1`. Use `0.1` for percentage or grade steps, `0.5` for half-marks.',
|
|
246
|
+
].join(' '),
|
|
247
|
+
control: 'number',
|
|
248
|
+
table: {
|
|
249
|
+
type: { summary: 'number' },
|
|
250
|
+
defaultValue: { summary: '1' },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
min: {
|
|
254
|
+
description: [
|
|
255
|
+
'Minimum allowed value. The minus spinner button is disabled when the current value',
|
|
256
|
+
'reaches this bound. On blur, any value below `min` is clamped up to `min`.',
|
|
257
|
+
'Default is `-Infinity` (no lower bound).',
|
|
258
|
+
].join(' '),
|
|
259
|
+
control: 'number',
|
|
260
|
+
table: {
|
|
261
|
+
type: { summary: 'number' },
|
|
262
|
+
defaultValue: { summary: '-Infinity' },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
max: {
|
|
266
|
+
description: [
|
|
267
|
+
'Maximum allowed value. The plus spinner button is disabled when the current value',
|
|
268
|
+
'reaches this bound. On blur, any value above `max` is clamped down to `max`.',
|
|
269
|
+
'Default is `Infinity` (no upper bound).',
|
|
270
|
+
].join(' '),
|
|
271
|
+
control: 'number',
|
|
272
|
+
table: {
|
|
273
|
+
type: { summary: 'number' },
|
|
274
|
+
defaultValue: { summary: 'Infinity' },
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
placeholder: {
|
|
278
|
+
description: [
|
|
279
|
+
'Hint text displayed when the field is empty.',
|
|
280
|
+
'Never use placeholder as the only visible label — it disappears on input.',
|
|
281
|
+
'Always pair with a visible label or `aria-label`.',
|
|
282
|
+
].join(' '),
|
|
283
|
+
control: 'text',
|
|
284
|
+
table: {
|
|
285
|
+
type: { summary: 'string' },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
value: {
|
|
289
|
+
description: [
|
|
290
|
+
'Controlled value — pair with `onChange` and do NOT also pass `defaultValue`.',
|
|
291
|
+
'If `defaultValue` is truthy, subsequent `value` prop changes are silently ignored.',
|
|
292
|
+
'For uncontrolled usage, use `defaultValue` instead.',
|
|
293
|
+
].join(' '),
|
|
294
|
+
control: 'text',
|
|
295
|
+
table: {
|
|
296
|
+
type: { summary: 'string | number' },
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
defaultValue: {
|
|
300
|
+
description: [
|
|
301
|
+
'Uncontrolled initial value. When truthy, all subsequent `value` prop changes are ignored',
|
|
302
|
+
'— the internal guard is `if (!defaultValue) { setValue(passedValue) }`.',
|
|
303
|
+
'For controlled usage, omit `defaultValue` entirely and use `value` + `onChange`.',
|
|
304
|
+
].join(' '),
|
|
305
|
+
control: 'text',
|
|
306
|
+
table: {
|
|
307
|
+
type: { summary: 'string | number' },
|
|
308
|
+
defaultValue: { summary: "''" },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
onChange: {
|
|
312
|
+
description: [
|
|
313
|
+
'Called via internal `useEffect` when the value changes.',
|
|
314
|
+
'The synthetic event is manually constructed — ONLY `e.currentTarget.value` is populated.',
|
|
315
|
+
'Do NOT access `e.target.value` or `e.nativeEvent` — they will be undefined or throw.',
|
|
316
|
+
].join(' '),
|
|
317
|
+
table: {
|
|
318
|
+
type: { summary: '(e: ChangeEvent<HTMLInputElement>) => void' },
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
className: {
|
|
322
|
+
description: [
|
|
323
|
+
'CSS class name applied to the inner `<input>` element only (not the container).',
|
|
324
|
+
'Use `containerClassName` to style the outer wrapper instead.',
|
|
325
|
+
].join(' '),
|
|
326
|
+
control: 'text',
|
|
327
|
+
table: {
|
|
328
|
+
type: { summary: 'string' },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
9
331
|
},
|
|
10
|
-
tags: ['autodocs'],
|
|
11
332
|
args: {
|
|
12
|
-
|
|
333
|
+
onChange: fn(),
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
export default meta;
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Helper: attach a per-story description to docs
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
const withDescription = (story, description) => ({
|
|
341
|
+
...story,
|
|
342
|
+
parameters: {
|
|
343
|
+
...story.parameters,
|
|
344
|
+
docs: {
|
|
345
|
+
...story.parameters?.docs,
|
|
346
|
+
description: { story: description },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Stateful template components
|
|
352
|
+
// Named components avoid hooks-in-callbacks lint issues (react-hooks plugin
|
|
353
|
+
// is NOT configured in this project — do not add eslint-disable comments).
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
const ControlledInputTemplate = () => {
|
|
356
|
+
const [attendanceTarget, setAttendanceTarget] = useState('96');
|
|
357
|
+
return (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Attendance target (%) \u2014 controlled from parent state" }), _jsx(NumberInput, { id: "attendance-target-controlled", "aria-label": "Attendance target percentage", value: attendanceTarget, min: 0, max: 100, step: 1, onChange: e => setAttendanceTarget(e.currentTarget.value) }), _jsxs("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: ["Parent state value:", ' ', _jsxs("strong", { children: [attendanceTarget, "%"] })] }), Number(attendanceTarget) < 90 && (_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-destructive-600)', fontSize: 'var(--font-size-2-13)' }, children: "Warning: targets below 90% are below national guidance" })), Number(attendanceTarget) >= 96 && (_jsx("p", { style: { margin: 0, color: 'var(--color-semantic-success-600)', fontSize: 'var(--font-size-2-13)' }, children: "Excellent \u2014 target meets Arbor recommended threshold" }))] }));
|
|
358
|
+
};
|
|
359
|
+
const DecimalStepTemplate = () => {
|
|
360
|
+
const [weightingA, setWeightingA] = useState('0.4');
|
|
361
|
+
const [weightingB, setWeightingB] = useState('0.6');
|
|
362
|
+
const total = weightingA !== '' && weightingB !== ''
|
|
363
|
+
? (parseFloat(weightingA || '0') + parseFloat(weightingB || '0')).toFixed(10)
|
|
364
|
+
: '—';
|
|
365
|
+
// Show the floating-point problem vs Decimal.js precision
|
|
366
|
+
const rawJsTotal = (parseFloat(weightingA || '0') + parseFloat(weightingB || '0'));
|
|
367
|
+
const formattedTotal = isNaN(rawJsTotal) ? '—' : rawJsTotal.toString();
|
|
368
|
+
return (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Adjust the grade weightings using step=0.1. Both fields use Decimal.js internally for precision." }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "weighting-component-a", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Component A weighting" }), _jsx(NumberInput, { id: "weighting-component-a", value: weightingA, min: 0, max: 1, step: 0.1, onChange: e => setWeightingA(e.currentTarget.value) })] }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "weighting-component-b", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Component B weighting" }), _jsx(NumberInput, { id: "weighting-component-b", value: weightingB, min: 0, max: 1, step: 0.1, onChange: e => setWeightingB(e.currentTarget.value) })] }), _jsxs("div", { style: { padding: 'var(--spacing-small)', background: 'var(--color-grey-050)', borderRadius: 'var(--border-radius-small)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsxs("p", { style: { margin: 0, fontSize: 'var(--font-size-2-13)', color: 'var(--color-grey-600)' }, children: ["Native JS addition result:", ' ', _jsx("strong", { style: { fontFamily: 'monospace' }, children: formattedTotal })] }), _jsxs("p", { style: { margin: 0, fontSize: 'var(--font-size-2-13)', color: 'var(--color-grey-600)' }, children: ["With Decimal.js (what spinners use):", ' ', _jsx("strong", { style: { fontFamily: 'monospace' }, children: total })] }), _jsx("p", { style: { margin: 0, fontSize: 'var(--font-size-1-11)', color: 'var(--color-grey-500)', fontStyle: 'italic' }, children: "Try stepping 0.1 + 0.2 \u2014 native JS gives 0.30000000000000004; Decimal.js gives 0.3" })] })] }));
|
|
369
|
+
};
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Stories
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
export const Default = withDescription({
|
|
374
|
+
args: {
|
|
375
|
+
placeholder: 'Enter a number',
|
|
13
376
|
step: 1,
|
|
14
377
|
disabled: false,
|
|
15
378
|
hasError: false,
|
|
16
|
-
|
|
17
|
-
min: 0,
|
|
18
|
-
max: 10,
|
|
379
|
+
disableSpinners: false,
|
|
19
380
|
},
|
|
20
|
-
}
|
|
21
|
-
|
|
381
|
+
render: args => _jsx(NumberInput, { ...args }),
|
|
382
|
+
}, 'The interactive canvas story — every prop is wired to the **Controls** panel. Use the controls to explore the error state, disabled state, spinner visibility, step size, and min/max bounds.');
|
|
383
|
+
export const Disabled = withDescription({
|
|
384
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "absent-count-disabled", style: { color: 'var(--color-grey-400)', fontSize: 'var(--font-size-2-13)' }, children: "Absent pupils (read-only period)" }), _jsx(NumberInput, { id: "absent-count-disabled", defaultValue: 3, min: 0, disabled: true })] }), _jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)', fontStyle: 'italic' }, children: "The attendance register is locked after the submission window closes. Both spinner buttons and the text input are non-interactive." })] })),
|
|
385
|
+
parameters: {
|
|
386
|
+
docs: {
|
|
387
|
+
source: {
|
|
388
|
+
language: 'tsx',
|
|
389
|
+
code: [
|
|
390
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
391
|
+
'',
|
|
392
|
+
'function DisabledExample() {',
|
|
393
|
+
' return (',
|
|
394
|
+
' <div style={{ display: "flex", flexDirection: "column", gap: "var(--spacing-xsmall)" }}>',
|
|
395
|
+
' <label htmlFor="absent-count-disabled" style={{ color: "var(--color-grey-400)", fontSize: "var(--font-size-2-13)" }}>',
|
|
396
|
+
' Absent pupils (read-only period)',
|
|
397
|
+
' </label>',
|
|
398
|
+
' <NumberInput',
|
|
399
|
+
' id="absent-count-disabled"',
|
|
400
|
+
' defaultValue={3}',
|
|
401
|
+
' min={0}',
|
|
402
|
+
' disabled',
|
|
403
|
+
' />',
|
|
404
|
+
' </div>',
|
|
405
|
+
' );',
|
|
406
|
+
'}',
|
|
407
|
+
'export default DisabledExample;',
|
|
408
|
+
].join('\n'),
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
}, '`disabled={true}` applies `ds-number-input__container--disabled` to the container, greys out all elements, and disables both spinner buttons. The input is removed from the tab order and its value is excluded from form submission. Use this after a register submission window closes, or when a field depends on a condition that has not been met.');
|
|
413
|
+
export const WithError = withDescription({
|
|
414
|
+
render: () => (_jsx("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "exam-mark-error", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Exam mark out of 100" }), _jsx(NumberInput, { id: "exam-mark-error", defaultValue: 150, min: 0, max: 100, hasError: true, "aria-invalid": "true", "aria-describedby": "exam-mark-error-msg" }), _jsx("span", { id: "exam-mark-error-msg", style: { color: 'var(--color-semantic-destructive-600)', fontSize: 'var(--font-size-1-11)' }, children: "Mark must be between 0 and 100" })] }) })),
|
|
415
|
+
parameters: {
|
|
416
|
+
docs: {
|
|
417
|
+
source: {
|
|
418
|
+
language: 'tsx',
|
|
419
|
+
code: [
|
|
420
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
421
|
+
'',
|
|
422
|
+
'function WithErrorExample() {',
|
|
423
|
+
' return (',
|
|
424
|
+
' <div style={{ display: "flex", flexDirection: "column", gap: "var(--spacing-xsmall)" }}>',
|
|
425
|
+
' <label htmlFor="exam-mark-error" style={{ color: "var(--color-grey-600)", fontSize: "var(--font-size-2-13)" }}>',
|
|
426
|
+
' Exam mark out of 100',
|
|
427
|
+
' </label>',
|
|
428
|
+
' <NumberInput',
|
|
429
|
+
' id="exam-mark-error"',
|
|
430
|
+
' defaultValue={150}',
|
|
431
|
+
' min={0}',
|
|
432
|
+
' max={100}',
|
|
433
|
+
' hasError',
|
|
434
|
+
' aria-invalid="true"',
|
|
435
|
+
' aria-describedby="exam-mark-error-msg"',
|
|
436
|
+
' />',
|
|
437
|
+
' <span',
|
|
438
|
+
' id="exam-mark-error-msg"',
|
|
439
|
+
' style={{ color: "var(--color-semantic-destructive-600)", fontSize: "var(--font-size-1-11)" }}',
|
|
440
|
+
' >',
|
|
441
|
+
' Mark must be between 0 and 100',
|
|
442
|
+
' </span>',
|
|
443
|
+
' </div>',
|
|
444
|
+
' );',
|
|
445
|
+
'}',
|
|
446
|
+
'export default WithErrorExample;',
|
|
447
|
+
].join('\n'),
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
}, [
|
|
452
|
+
'`hasError={true}` applies `ds-number-input__container--error`, which adds a red border to the container.',
|
|
453
|
+
'`hasError` is **purely visual** — it carries no ARIA semantics. When using bare NumberInput outside FormField,',
|
|
454
|
+
'always also set `aria-invalid="true"` and `aria-describedby` pointing to the error message element.',
|
|
455
|
+
'FormField handles all three automatically when `errorText` is present.',
|
|
456
|
+
].join(' '));
|
|
457
|
+
export const WithMinMax = withDescription({
|
|
458
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "class-size", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Class size (1\u201340 pupils)" }), _jsx(NumberInput, { id: "class-size", defaultValue: 5, min: 1, max: 40, step: 1 })] }), _jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)', fontStyle: 'italic' }, children: "Try typing 99 and tabbing away \u2014 the value clamps to 40 on blur. The plus button disables at 40; the minus button disables at 1." }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "attendance-threshold", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Attendance alert threshold % (50\u2013100)" }), _jsx(NumberInput, { id: "attendance-threshold", defaultValue: 90, min: 50, max: 100, step: 1 })] })] })),
|
|
459
|
+
parameters: {
|
|
460
|
+
docs: {
|
|
461
|
+
source: {
|
|
462
|
+
language: 'tsx',
|
|
463
|
+
code: [
|
|
464
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
465
|
+
'',
|
|
466
|
+
'function WithMinMaxExample() {',
|
|
467
|
+
' return (',
|
|
468
|
+
' <NumberInput',
|
|
469
|
+
' id="class-size"',
|
|
470
|
+
' defaultValue={5}',
|
|
471
|
+
' min={1}',
|
|
472
|
+
' max={40}',
|
|
473
|
+
' step={1}',
|
|
474
|
+
' />',
|
|
475
|
+
' );',
|
|
476
|
+
'}',
|
|
477
|
+
'export default WithMinMaxExample;',
|
|
478
|
+
].join('\n'),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
}, [
|
|
483
|
+
'Setting `min` and `max` enables automatic clamping.',
|
|
484
|
+
'On blur (or after a spinner click that would exceed the bound),',
|
|
485
|
+
'the value is snapped to the nearest allowed boundary using Decimal.js arithmetic.',
|
|
486
|
+
'The minus button disables when `value <= min`; the plus button disables when `value >= max`.',
|
|
487
|
+
'Try typing a value outside the range and tabbing away to see clamping in action.',
|
|
488
|
+
].join(' '));
|
|
489
|
+
export const DecimalStep = withDescription({
|
|
490
|
+
render: () => _jsx(DecimalStepTemplate, {}),
|
|
491
|
+
parameters: {
|
|
492
|
+
docs: {
|
|
493
|
+
source: {
|
|
494
|
+
language: 'tsx',
|
|
495
|
+
code: [
|
|
496
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
497
|
+
'',
|
|
498
|
+
'function DecimalStepExample() {',
|
|
499
|
+
' return (',
|
|
500
|
+
' <NumberInput',
|
|
501
|
+
' id="grade-weighting"',
|
|
502
|
+
' defaultValue={0}',
|
|
503
|
+
' min={0}',
|
|
504
|
+
' max={1}',
|
|
505
|
+
' step={0.1}',
|
|
506
|
+
' aria-label="Grade component weighting"',
|
|
507
|
+
' />',
|
|
508
|
+
' );',
|
|
509
|
+
'}',
|
|
510
|
+
'export default DecimalStepExample;',
|
|
511
|
+
].join('\n'),
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
}, [
|
|
516
|
+
'This story demonstrates why NumberInput uses **Decimal.js** for all spinner arithmetic.',
|
|
517
|
+
'Native JavaScript floating-point arithmetic means `0.1 + 0.2 = 0.30000000000000004`.',
|
|
518
|
+
'For grade weightings, mark totals, or percentage thresholds, that kind of drift causes',
|
|
519
|
+
'real problems. The spinner buttons in this story use `step={0.1}` — try stepping from 0.1',
|
|
520
|
+
'and watch the Decimal.js result stay precise while the native JS equivalent drifts.',
|
|
521
|
+
'The live comparison panel makes the difference visible.',
|
|
522
|
+
].join(' '));
|
|
523
|
+
export const SpinnersDisabled = withDescription({
|
|
524
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "pupil-premium-count", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Pupil premium count" }), _jsx(NumberInput, { id: "pupil-premium-count", defaultValue: 47, disableSpinners: true, "aria-label": "Number of pupil premium eligible pupils" })] }), _jsx("p", { style: { margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-1-11)', fontStyle: 'italic' }, children: "No spinner buttons \u2014 keyboard entry only. Min/max and blur-clamping still apply." }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }, children: [_jsx("label", { htmlFor: "form-capacity", style: { color: 'var(--color-grey-600)', fontSize: 'var(--font-size-2-13)' }, children: "Form capacity (grid cell \u2014 spinners would crowd the column)" }), _jsx("div", { style: { width: '6rem' }, children: _jsx(NumberInput, { id: "form-capacity", defaultValue: 30, min: 1, max: 60, disableSpinners: true, "aria-label": "Form group capacity" }) })] })] })),
|
|
525
|
+
parameters: {
|
|
526
|
+
docs: {
|
|
527
|
+
source: {
|
|
528
|
+
language: 'tsx',
|
|
529
|
+
code: [
|
|
530
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
531
|
+
'',
|
|
532
|
+
'function SpinnersDisabledExample() {',
|
|
533
|
+
' return (',
|
|
534
|
+
' <NumberInput',
|
|
535
|
+
' id="pupil-premium-count"',
|
|
536
|
+
' defaultValue={47}',
|
|
537
|
+
' disableSpinners',
|
|
538
|
+
' aria-label="Number of pupil premium eligible pupils"',
|
|
539
|
+
' />',
|
|
540
|
+
' );',
|
|
541
|
+
'}',
|
|
542
|
+
'export default SpinnersDisabledExample;',
|
|
543
|
+
].join('\n'),
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
}, [
|
|
548
|
+
'`disableSpinners={true}` hides the minus and plus buttons, leaving a clean text-entry field.',
|
|
549
|
+
'Use this in data-grid cells, compact filter panels, or anywhere the ± buttons would be too large',
|
|
550
|
+
'or visually noisy. All other behaviour — blur-clamping, `min`/`max` bounds, Decimal.js arithmetic —',
|
|
551
|
+
'continues to work exactly as normal. Keyboard users simply type the value directly.',
|
|
552
|
+
].join(' '));
|
|
553
|
+
export const InFormField = withDescription({
|
|
554
|
+
render: () => (_jsxs("div", { style: { maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }, children: [_jsx(FormField, { inputType: "number", label: "Target attendance (%)", id: "target-attendance", fieldDescription: "Set the school-wide attendance target. Pupils below this threshold will appear in the at-risk register.", inputProps: { defaultValue: 96, min: 50, max: 100, step: 1 } }), _jsx(FormField, { inputType: "number", label: "Absence alert after (days)", id: "absence-alert-days", inputProps: { defaultValue: 3, min: 1, step: 1 } }), _jsx(FormField, { inputType: "number", label: "Maximum class size", id: "max-class-size", errorText: "Class size must be between 1 and 40", inputProps: { defaultValue: 50, min: 1, max: 40, step: 1 } })] })),
|
|
555
|
+
parameters: {
|
|
556
|
+
docs: {
|
|
557
|
+
source: {
|
|
558
|
+
language: 'tsx',
|
|
559
|
+
code: [
|
|
560
|
+
"import { FormField } from '@arbor-education/design-system.components';",
|
|
561
|
+
'',
|
|
562
|
+
'function InFormFieldExample() {',
|
|
563
|
+
' return (',
|
|
564
|
+
' <div>',
|
|
565
|
+
' <FormField',
|
|
566
|
+
' inputType="number"',
|
|
567
|
+
' label="Target attendance (%)"',
|
|
568
|
+
' id="target-attendance"',
|
|
569
|
+
' fieldDescription="Set the school-wide attendance target."',
|
|
570
|
+
' inputProps={{ defaultValue: 96, min: 50, max: 100, step: 1 }}',
|
|
571
|
+
' />',
|
|
572
|
+
' <FormField',
|
|
573
|
+
' inputType="number"',
|
|
574
|
+
' label="Maximum class size"',
|
|
575
|
+
' id="max-class-size"',
|
|
576
|
+
' errorText="Class size must be between 1 and 40"',
|
|
577
|
+
' inputProps={{ defaultValue: 50, min: 1, max: 40, step: 1 }}',
|
|
578
|
+
' />',
|
|
579
|
+
' </div>',
|
|
580
|
+
' );',
|
|
581
|
+
'}',
|
|
582
|
+
'export default InFormFieldExample;',
|
|
583
|
+
].join('\n'),
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
}, [
|
|
588
|
+
'**Always use `FormField` with `inputType="number"` in real forms.**',
|
|
589
|
+
'FormField automatically wires `hasError` (from `errorText`), `aria-invalid`, `aria-describedby`,',
|
|
590
|
+
'and the label `htmlFor` association — you do not need to set any of these manually in `inputProps`.',
|
|
591
|
+
'The third field shows how `errorText` drives the error state. The `fieldDescription` prop in the',
|
|
592
|
+
'first field renders a hint beneath the label, automatically linked via `aria-describedby`.',
|
|
593
|
+
].join(' '));
|
|
594
|
+
export const ControlledInput = withDescription({
|
|
595
|
+
render: () => _jsx(ControlledInputTemplate, {}),
|
|
596
|
+
parameters: {
|
|
597
|
+
docs: {
|
|
598
|
+
source: {
|
|
599
|
+
language: 'tsx',
|
|
600
|
+
code: [
|
|
601
|
+
"import { useState } from 'react';",
|
|
602
|
+
"import { NumberInput } from '@arbor-education/design-system.components';",
|
|
603
|
+
'',
|
|
604
|
+
'function AttendanceTargetField() {',
|
|
605
|
+
' const [target, setTarget] = useState(\'96\');',
|
|
606
|
+
'',
|
|
607
|
+
' return (',
|
|
608
|
+
' <div>',
|
|
609
|
+
' <label htmlFor="attendance-target">Attendance target (%)</label>',
|
|
610
|
+
' <NumberInput',
|
|
611
|
+
' id="attendance-target"',
|
|
612
|
+
' value={target}',
|
|
613
|
+
' min={0}',
|
|
614
|
+
' max={100}',
|
|
615
|
+
' step={1}',
|
|
616
|
+
' // NOTE: only e.currentTarget.value is populated — do not use e.target.value',
|
|
617
|
+
' onChange={(e) => setTarget(e.currentTarget.value)}',
|
|
618
|
+
' />',
|
|
619
|
+
' <p>Current target: {target}%</p>',
|
|
620
|
+
' </div>',
|
|
621
|
+
' );',
|
|
622
|
+
'}',
|
|
623
|
+
'export default AttendanceTargetField;',
|
|
624
|
+
].join('\n'),
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
}, [
|
|
629
|
+
'Demonstrates the **controlled** pattern: `value` is driven from `useState`, updated via `onChange`.',
|
|
630
|
+
'There is no `defaultValue` prop — omitting it is essential, as a truthy `defaultValue`',
|
|
631
|
+
'would silently block all `value` prop updates after mount.',
|
|
632
|
+
'Note the `onChange` handler reads from `e.currentTarget.value`, not `e.target.value`',
|
|
633
|
+
'— the event is manually constructed inside NumberInput and only `currentTarget.value` is set.',
|
|
634
|
+
'The feedback panel below the input reads the live state value, confirming the controlled link.',
|
|
635
|
+
].join(' '));
|
|
22
636
|
//# sourceMappingURL=NumberInput.stories.js.map
|