@auto-engineer/component-implementor-react 1.98.0 → 1.100.0
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +92 -0
- package/dist/src/commands/implement-component.d.ts +19 -0
- package/dist/src/commands/implement-component.d.ts.map +1 -1
- package/dist/src/commands/implement-component.js +109 -30
- package/dist/src/commands/implement-component.js.map +1 -1
- package/dist/src/commands/implement-component.test.js +259 -69
- package/dist/src/commands/implement-component.test.js.map +1 -1
- package/dist/src/extract-exports.d.ts +6 -0
- package/dist/src/extract-exports.d.ts.map +1 -0
- package/dist/src/extract-exports.js +46 -0
- package/dist/src/extract-exports.js.map +1 -0
- package/dist/src/generate-story-deterministic.d.ts +30 -0
- package/dist/src/generate-story-deterministic.d.ts.map +1 -0
- package/dist/src/generate-story-deterministic.js +229 -0
- package/dist/src/generate-story-deterministic.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/pipeline/run-pipeline.d.ts +69 -0
- package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
- package/dist/src/pipeline/run-pipeline.js +78 -0
- package/dist/src/pipeline/run-pipeline.js.map +1 -0
- package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
- package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
- package/dist/src/pipeline/run-pipeline.test.js +247 -0
- package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
- package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-component.js +50 -0
- package/dist/src/pipeline/steps/generate-component.js.map +1 -0
- package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-component.test.js +106 -0
- package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
- package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-story.js +14 -0
- package/dist/src/pipeline/steps/generate-story.js.map +1 -0
- package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-story.test.js +41 -0
- package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
- package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-test.js +19 -0
- package/dist/src/pipeline/steps/generate-test.js.map +1 -0
- package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/generate-test.test.js +60 -0
- package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
- package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
- package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
- package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
- package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
- package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.js +22 -0
- package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
- package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
- package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
- package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
- package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
- package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
- package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
- package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.js +4 -0
- package/dist/src/pipeline/steps/visual-test.js.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
- package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
- package/dist/src/pipeline/steps/visual-test.test.js +9 -0
- package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
- package/dist/src/project-context.d.ts +10 -0
- package/dist/src/project-context.d.ts.map +1 -0
- package/dist/src/project-context.js +178 -0
- package/dist/src/project-context.js.map +1 -0
- package/dist/src/prompt.d.ts +39 -7
- package/dist/src/prompt.d.ts.map +1 -1
- package/dist/src/prompt.js +233 -23
- package/dist/src/prompt.js.map +1 -1
- package/dist/src/prompt.test.js +154 -9
- package/dist/src/prompt.test.js.map +1 -1
- package/dist/src/scaffold.d.ts +49 -0
- package/dist/src/scaffold.d.ts.map +1 -0
- package/dist/src/scaffold.js +208 -0
- package/dist/src/scaffold.js.map +1 -0
- package/dist/src/tools/lint-runner.d.ts +7 -0
- package/dist/src/tools/lint-runner.d.ts.map +1 -0
- package/dist/src/tools/lint-runner.js +48 -0
- package/dist/src/tools/lint-runner.js.map +1 -0
- package/dist/src/tools/lint-runner.test.d.ts +2 -0
- package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
- package/dist/src/tools/lint-runner.test.js +90 -0
- package/dist/src/tools/lint-runner.test.js.map +1 -0
- package/dist/src/tools/storybook-runner.d.ts +6 -0
- package/dist/src/tools/storybook-runner.d.ts.map +1 -0
- package/dist/src/tools/storybook-runner.js +25 -0
- package/dist/src/tools/storybook-runner.js.map +1 -0
- package/dist/src/tools/storybook-runner.test.d.ts +2 -0
- package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
- package/dist/src/tools/storybook-runner.test.js +43 -0
- package/dist/src/tools/storybook-runner.test.js.map +1 -0
- package/dist/src/tools/test-runner.d.ts +9 -0
- package/dist/src/tools/test-runner.d.ts.map +1 -0
- package/dist/src/tools/test-runner.js +74 -0
- package/dist/src/tools/test-runner.js.map +1 -0
- package/dist/src/tools/test-runner.test.d.ts +2 -0
- package/dist/src/tools/test-runner.test.d.ts.map +1 -0
- package/dist/src/tools/test-runner.test.js +177 -0
- package/dist/src/tools/test-runner.test.js.map +1 -0
- package/dist/src/tools/type-checker.d.ts +6 -0
- package/dist/src/tools/type-checker.d.ts.map +1 -0
- package/dist/src/tools/type-checker.js +36 -0
- package/dist/src/tools/type-checker.js.map +1 -0
- package/dist/src/tools/type-checker.test.d.ts +2 -0
- package/dist/src/tools/type-checker.test.d.ts.map +1 -0
- package/dist/src/tools/type-checker.test.js +96 -0
- package/dist/src/tools/type-checker.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/inputs/model-a/spec-deltas.json +1460 -0
- package/inputs/model-b/spec-deltas.json +1424 -0
- package/inputs/model-c/spec-deltas.json +1432 -0
- package/inputs/model-d/spec-deltas.json +967 -0
- package/inputs/model-e/spec-deltas.json +2292 -0
- package/ketchup-plan.md +43 -8
- package/package.json +3 -3
- package/scoring-heuristic.md +138 -0
- package/scripts/improve.ts +23 -18
- package/src/commands/implement-component.test.ts +309 -76
- package/src/commands/implement-component.ts +155 -31
- package/src/extract-exports.ts +53 -0
- package/src/generate-story-deterministic.ts +267 -0
- package/src/index.ts +12 -0
- package/src/pipeline/run-pipeline.test.ts +292 -0
- package/src/pipeline/run-pipeline.ts +160 -0
- package/src/pipeline/steps/generate-component.test.ts +130 -0
- package/src/pipeline/steps/generate-component.ts +60 -0
- package/src/pipeline/steps/generate-story.test.ts +54 -0
- package/src/pipeline/steps/generate-story.ts +17 -0
- package/src/pipeline/steps/generate-test.test.ts +75 -0
- package/src/pipeline/steps/generate-test.ts +25 -0
- package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
- package/src/pipeline/steps/lint-fix-loop.ts +59 -0
- package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
- package/src/pipeline/steps/story-fix-loop.ts +47 -0
- package/src/pipeline/steps/storybook-test.test.ts +82 -0
- package/src/pipeline/steps/storybook-test.ts +27 -0
- package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
- package/src/pipeline/steps/test-fix-loop.ts +56 -0
- package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
- package/src/pipeline/steps/type-fix-loop.ts +55 -0
- package/src/pipeline/steps/visual-test.test.ts +10 -0
- package/src/pipeline/steps/visual-test.ts +5 -0
- package/src/project-context.ts +205 -0
- package/src/prompt.test.ts +174 -8
- package/src/prompt.ts +301 -23
- package/src/scaffold.ts +281 -0
- package/src/tools/lint-runner.test.ts +112 -0
- package/src/tools/lint-runner.ts +52 -0
- package/src/tools/storybook-runner.test.ts +53 -0
- package/src/tools/storybook-runner.ts +29 -0
- package/src/tools/test-runner.test.ts +213 -0
- package/src/tools/test-runner.ts +84 -0
- package/src/tools/type-checker.test.ts +120 -0
- package/src/tools/type-checker.ts +42 -0
- package/vitest.config.ts +9 -1
- package/dist/src/generate-component.d.ts +0 -4
- package/dist/src/generate-component.d.ts.map +0 -1
- package/dist/src/generate-component.js +0 -14
- package/dist/src/generate-component.js.map +0 -1
- package/dist/src/generate-component.test.d.ts.map +0 -1
- package/dist/src/generate-component.test.js +0 -73
- package/dist/src/generate-component.test.js.map +0 -1
- package/dist/src/generate-story.d.ts +0 -4
- package/dist/src/generate-story.d.ts.map +0 -1
- package/dist/src/generate-story.js +0 -14
- package/dist/src/generate-story.js.map +0 -1
- package/dist/src/generate-story.test.d.ts.map +0 -1
- package/dist/src/generate-story.test.js +0 -58
- package/dist/src/generate-story.test.js.map +0 -1
- package/dist/src/generate-test.d.ts +0 -4
- package/dist/src/generate-test.d.ts.map +0 -1
- package/dist/src/generate-test.js +0 -14
- package/dist/src/generate-test.js.map +0 -1
- package/dist/src/generate-test.test.d.ts.map +0 -1
- package/dist/src/generate-test.test.js +0 -77
- package/dist/src/generate-test.test.js.map +0 -1
- package/dist/src/reconcile.d.ts +0 -8
- package/dist/src/reconcile.d.ts.map +0 -1
- package/dist/src/reconcile.js +0 -18
- package/dist/src/reconcile.js.map +0 -1
- package/dist/src/reconcile.test.d.ts +0 -2
- package/dist/src/reconcile.test.d.ts.map +0 -1
- package/dist/src/reconcile.test.js +0 -108
- package/dist/src/reconcile.test.js.map +0 -1
- package/src/generate-component.test.ts +0 -89
- package/src/generate-component.ts +0 -16
- package/src/generate-story.test.ts +0 -71
- package/src/generate-story.ts +0 -16
- package/src/generate-test.test.ts +0 -93
- package/src/generate-test.ts +0 -16
- package/src/reconcile.test.ts +0 -127
- package/src/reconcile.ts +0 -27
- /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
- /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
- /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"componentId": "exercise-selector",
|
|
4
|
+
"componentName": "ExerciseSelector",
|
|
5
|
+
"isNew": true,
|
|
6
|
+
"atomicType": "molecule",
|
|
7
|
+
"composes": ["ui-components-combobox"],
|
|
8
|
+
"specDeltas": {
|
|
9
|
+
"structure": [
|
|
10
|
+
"Composes Combobox to provide a searchable dropdown for selecting exercises",
|
|
11
|
+
"Renders as a Combobox with options prop passed from parent, where each option is { value: string, label: string }",
|
|
12
|
+
"Uses semantic <label> element associated with the input via htmlFor and id pairing",
|
|
13
|
+
"Root element is a <section> with role='region' and aria-label='Exercise Selector'"
|
|
14
|
+
],
|
|
15
|
+
"rendering": [
|
|
16
|
+
"Renders the selected exercise label in the Combobox value display",
|
|
17
|
+
"In loading state (isLoading=true), renders a Skeleton inside the input area with dimensions matching the input height and width",
|
|
18
|
+
"In error state (error is truthy), renders an Alert with destructive variant below the Combobox, displaying the error message",
|
|
19
|
+
"Hides the dropdown list when disabled=true"
|
|
20
|
+
],
|
|
21
|
+
"interaction": [
|
|
22
|
+
"Calls onSelect: (selected: { value: string, label: string }) => void when an exercise is selected from the dropdown",
|
|
23
|
+
"Typing in the input filters the options list case-insensitively, trimming whitespace, and shows all options when query is empty",
|
|
24
|
+
"Arrow down key opens the dropdown if closed, arrow keys navigate options, Enter selects the highlighted option",
|
|
25
|
+
"Escape closes the dropdown without changing selection",
|
|
26
|
+
"When disabled=true, prevents opening the dropdown and ignores input changes"
|
|
27
|
+
],
|
|
28
|
+
"styling": [
|
|
29
|
+
"Root section uses `flex flex-col gap-2` for layout, with `p-4 bg-background rounded-md shadow-sm`.",
|
|
30
|
+
"Label uses `text-sm font-medium text-foreground` with `mb-1`.",
|
|
31
|
+
"Combobox input uses `h-10 px-3 py-2 border border-input bg-background text-sm text-foreground rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
32
|
+
"Dropdown list uses `mt-1 bg-popover text-popover-foreground shadow-md rounded-md overflow-hidden z-[500]` with `useTransition(isOpen, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, leave: { opacity: 0, scale: 0.95 }, config: { tension: 400, friction: 22 } })` for animation; respects `useReducedMotion()` by setting `immediate: true` when reduced motion is preferred.",
|
|
33
|
+
"Each option in dropdown uses `px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer` with `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`; selected option uses `bg-accent text-accent-foreground`.",
|
|
34
|
+
"In loading state, skeleton uses `h-10 w-full bg-muted animate-pulse rounded-md`.",
|
|
35
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex items-center gap-2`.",
|
|
36
|
+
"When disabled, input uses `opacity-50 cursor-not-allowed`.",
|
|
37
|
+
"Touch targets for dropdown options are `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
38
|
+
"Focus on input uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
39
|
+
"Numeric displays (if any) use `font-variant-numeric: tabular-nums`."
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"props": [
|
|
43
|
+
{
|
|
44
|
+
"name": "options",
|
|
45
|
+
"type": "Array<{ value: string; label: string }>",
|
|
46
|
+
"required": true,
|
|
47
|
+
"description": "List of available exercises to select from.",
|
|
48
|
+
"category": "data"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "value",
|
|
52
|
+
"type": "string",
|
|
53
|
+
"required": false,
|
|
54
|
+
"default": "''",
|
|
55
|
+
"description": "Currently selected exercise value.",
|
|
56
|
+
"category": "state"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "onSelect",
|
|
60
|
+
"type": "(selected: { value: string; label: string }) => void",
|
|
61
|
+
"required": true,
|
|
62
|
+
"description": "Callback when an exercise is selected.",
|
|
63
|
+
"category": "callback"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "isLoading",
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"required": false,
|
|
69
|
+
"default": "false",
|
|
70
|
+
"description": "Indicates if options are loading.",
|
|
71
|
+
"category": "state"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "error",
|
|
75
|
+
"type": "string | null",
|
|
76
|
+
"required": false,
|
|
77
|
+
"default": "null",
|
|
78
|
+
"description": "Error message to display.",
|
|
79
|
+
"category": "state"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "disabled",
|
|
83
|
+
"type": "boolean",
|
|
84
|
+
"required": false,
|
|
85
|
+
"default": "false",
|
|
86
|
+
"description": "Disables the selector.",
|
|
87
|
+
"category": "state"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "placeholder",
|
|
91
|
+
"type": "string",
|
|
92
|
+
"required": false,
|
|
93
|
+
"default": "'Select exercise'",
|
|
94
|
+
"description": "Placeholder text for the input.",
|
|
95
|
+
"category": "config"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "size",
|
|
99
|
+
"type": "\"sm\" | \"md\" | \"lg\"",
|
|
100
|
+
"required": false,
|
|
101
|
+
"default": "\"md\"",
|
|
102
|
+
"description": "Controls the size variant of the selector.",
|
|
103
|
+
"category": "visual"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"storyVariants": [
|
|
107
|
+
{
|
|
108
|
+
"name": "Default",
|
|
109
|
+
"description": "Ready state with options.",
|
|
110
|
+
"args": {
|
|
111
|
+
"options": [
|
|
112
|
+
{
|
|
113
|
+
"value": "bench-press",
|
|
114
|
+
"label": "Bench Press"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"value": "squat",
|
|
118
|
+
"label": "Squat"
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"value": "",
|
|
122
|
+
"onSelect": "fn()",
|
|
123
|
+
"isLoading": false,
|
|
124
|
+
"error": null,
|
|
125
|
+
"disabled": false,
|
|
126
|
+
"placeholder": "Select exercise",
|
|
127
|
+
"size": "md"
|
|
128
|
+
},
|
|
129
|
+
"needsPlayFunction": false
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "Loading",
|
|
133
|
+
"description": "Loading state.",
|
|
134
|
+
"args": {
|
|
135
|
+
"options": [
|
|
136
|
+
{
|
|
137
|
+
"value": "bench-press",
|
|
138
|
+
"label": "Bench Press"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"value": "squat",
|
|
142
|
+
"label": "Squat"
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"value": "",
|
|
146
|
+
"onSelect": "fn()",
|
|
147
|
+
"isLoading": true,
|
|
148
|
+
"error": null,
|
|
149
|
+
"disabled": false,
|
|
150
|
+
"placeholder": "Select exercise",
|
|
151
|
+
"size": "md"
|
|
152
|
+
},
|
|
153
|
+
"needsPlayFunction": false
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "Error",
|
|
157
|
+
"description": "Error state.",
|
|
158
|
+
"args": {
|
|
159
|
+
"options": [
|
|
160
|
+
{
|
|
161
|
+
"value": "bench-press",
|
|
162
|
+
"label": "Bench Press"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"value": "squat",
|
|
166
|
+
"label": "Squat"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"value": "",
|
|
170
|
+
"onSelect": "fn()",
|
|
171
|
+
"isLoading": false,
|
|
172
|
+
"error": "Failed to load exercises",
|
|
173
|
+
"disabled": false,
|
|
174
|
+
"placeholder": "Select exercise",
|
|
175
|
+
"size": "md"
|
|
176
|
+
},
|
|
177
|
+
"needsPlayFunction": false
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "Disabled",
|
|
181
|
+
"description": "Disabled state.",
|
|
182
|
+
"args": {
|
|
183
|
+
"options": [
|
|
184
|
+
{
|
|
185
|
+
"value": "bench-press",
|
|
186
|
+
"label": "Bench Press"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"value": "squat",
|
|
190
|
+
"label": "Squat"
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
"value": "bench-press",
|
|
194
|
+
"onSelect": "fn()",
|
|
195
|
+
"isLoading": false,
|
|
196
|
+
"error": null,
|
|
197
|
+
"disabled": true,
|
|
198
|
+
"placeholder": "Select exercise",
|
|
199
|
+
"size": "md"
|
|
200
|
+
},
|
|
201
|
+
"needsPlayFunction": false
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "InteractiveSelection",
|
|
205
|
+
"description": "Selecting an option.",
|
|
206
|
+
"args": {
|
|
207
|
+
"options": [
|
|
208
|
+
{
|
|
209
|
+
"value": "bench-press",
|
|
210
|
+
"label": "Bench Press"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"value": "squat",
|
|
214
|
+
"label": "Squat"
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
"value": "",
|
|
218
|
+
"onSelect": "fn()",
|
|
219
|
+
"isLoading": false,
|
|
220
|
+
"error": null,
|
|
221
|
+
"disabled": false,
|
|
222
|
+
"placeholder": "Select exercise",
|
|
223
|
+
"size": "md"
|
|
224
|
+
},
|
|
225
|
+
"needsPlayFunction": true,
|
|
226
|
+
"playDescription": "1. Click the combobox trigger to open dropdown, 2. userEvent.click(getByRole('option', { name: 'Bench Press' })), 3. await waitFor(() => expect(getByRole('combobox')).toHaveValue('Bench Press'))"
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
"dataContract": {
|
|
230
|
+
"source": "props",
|
|
231
|
+
"propsFieldName": "options",
|
|
232
|
+
"fields": ["value", "label"]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"componentId": "time-period-selector",
|
|
237
|
+
"componentName": "TimePeriodSelector",
|
|
238
|
+
"isNew": true,
|
|
239
|
+
"atomicType": "molecule",
|
|
240
|
+
"composes": ["ui-components-select"],
|
|
241
|
+
"specDeltas": {
|
|
242
|
+
"structure": [
|
|
243
|
+
"Composes Select to provide a dropdown for selecting time periods",
|
|
244
|
+
"Options are predefined periods like 'Last Week', 'Last Month', etc., passed as { value: string, label: string }[]",
|
|
245
|
+
"Uses semantic <label> element associated with the select via htmlFor and id pairing",
|
|
246
|
+
"Root element is a <section> with role='region' and aria-label='Time Period Selector'"
|
|
247
|
+
],
|
|
248
|
+
"rendering": [
|
|
249
|
+
"Renders the selected period label in the Select value display",
|
|
250
|
+
"In disabled state (disabled=true), renders the select as disabled"
|
|
251
|
+
],
|
|
252
|
+
"interaction": [
|
|
253
|
+
"Calls onSelect: (selected: string) => void when a time period is selected",
|
|
254
|
+
"When disabled=true, prevents selection changes"
|
|
255
|
+
],
|
|
256
|
+
"styling": [
|
|
257
|
+
"Root section uses `flex flex-col gap-2` for layout, with `p-4 bg-background rounded-md shadow-sm`.",
|
|
258
|
+
"Label uses `text-sm font-medium text-foreground` with `mb-1`.",
|
|
259
|
+
"Select trigger uses `h-10 px-3 py-2 border border-input bg-background text-sm text-foreground rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center justify-between`.",
|
|
260
|
+
"Select content uses `bg-popover text-popover-foreground shadow-md rounded-md overflow-hidden z-[500]` with `useTransition(isOpen, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, leave: { opacity: 0, scale: 0.95 }, config: { tension: 400, friction: 22 } })` for animation; respects `useReducedMotion()` by setting `immediate: true` when reduced motion is preferred.",
|
|
261
|
+
"Each select item uses `px-4 py-2 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer` with `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`; selected item uses `bg-accent text-accent-foreground`.",
|
|
262
|
+
"When disabled, trigger uses `opacity-50 cursor-not-allowed`.",
|
|
263
|
+
"Touch targets for select items are `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
264
|
+
"Focus on trigger uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
265
|
+
"Chevron icon in trigger uses `h-4 w-4 text-muted-foreground`.",
|
|
266
|
+
"Text in items uses `text-balance` for balanced wrapping."
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
"props": [
|
|
270
|
+
{
|
|
271
|
+
"name": "options",
|
|
272
|
+
"type": "Array<{ value: string; label: string }>",
|
|
273
|
+
"required": true,
|
|
274
|
+
"description": "List of time period options.",
|
|
275
|
+
"category": "data"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"name": "value",
|
|
279
|
+
"type": "string",
|
|
280
|
+
"required": false,
|
|
281
|
+
"default": "''",
|
|
282
|
+
"description": "Currently selected time period.",
|
|
283
|
+
"category": "state"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "onSelect",
|
|
287
|
+
"type": "(selected: string) => void",
|
|
288
|
+
"required": true,
|
|
289
|
+
"description": "Callback when a time period is selected.",
|
|
290
|
+
"category": "callback"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "disabled",
|
|
294
|
+
"type": "boolean",
|
|
295
|
+
"required": false,
|
|
296
|
+
"default": "false",
|
|
297
|
+
"description": "Disables the selector.",
|
|
298
|
+
"category": "state"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "placeholder",
|
|
302
|
+
"type": "string",
|
|
303
|
+
"required": false,
|
|
304
|
+
"default": "'Select period'",
|
|
305
|
+
"description": "Placeholder text.",
|
|
306
|
+
"category": "config"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "variant",
|
|
310
|
+
"type": "\"default\" | \"outline\"",
|
|
311
|
+
"required": false,
|
|
312
|
+
"default": "\"default\"",
|
|
313
|
+
"description": "Controls the visual variant of the selector.",
|
|
314
|
+
"category": "visual"
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
"storyVariants": [
|
|
318
|
+
{
|
|
319
|
+
"name": "Default",
|
|
320
|
+
"description": "Ready state with options.",
|
|
321
|
+
"args": {
|
|
322
|
+
"options": [
|
|
323
|
+
{
|
|
324
|
+
"value": "week",
|
|
325
|
+
"label": "Last Week"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"value": "month",
|
|
329
|
+
"label": "Last Month"
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
"value": "",
|
|
333
|
+
"onSelect": "fn()",
|
|
334
|
+
"disabled": false,
|
|
335
|
+
"placeholder": "Select period",
|
|
336
|
+
"variant": "default"
|
|
337
|
+
},
|
|
338
|
+
"needsPlayFunction": false
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "Disabled",
|
|
342
|
+
"description": "Disabled state.",
|
|
343
|
+
"args": {
|
|
344
|
+
"options": [
|
|
345
|
+
{
|
|
346
|
+
"value": "week",
|
|
347
|
+
"label": "Last Week"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"value": "month",
|
|
351
|
+
"label": "Last Month"
|
|
352
|
+
}
|
|
353
|
+
],
|
|
354
|
+
"value": "week",
|
|
355
|
+
"onSelect": "fn()",
|
|
356
|
+
"disabled": true,
|
|
357
|
+
"placeholder": "Select period",
|
|
358
|
+
"variant": "default"
|
|
359
|
+
},
|
|
360
|
+
"needsPlayFunction": false
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
"name": "InteractiveSelection",
|
|
364
|
+
"description": "Selecting a period.",
|
|
365
|
+
"args": {
|
|
366
|
+
"options": [
|
|
367
|
+
{
|
|
368
|
+
"value": "week",
|
|
369
|
+
"label": "Last Week"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"value": "month",
|
|
373
|
+
"label": "Last Month"
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
"value": "",
|
|
377
|
+
"onSelect": "fn()",
|
|
378
|
+
"disabled": false,
|
|
379
|
+
"placeholder": "Select period",
|
|
380
|
+
"variant": "default"
|
|
381
|
+
},
|
|
382
|
+
"needsPlayFunction": true,
|
|
383
|
+
"playDescription": "1. Click the select trigger to open dropdown, 2. userEvent.click(getByRole('option', { name: 'Last Week' })), 3. await waitFor(() => expect(getByRole('combobox')).toHaveValue('Last Week'))"
|
|
384
|
+
}
|
|
385
|
+
],
|
|
386
|
+
"dataContract": {
|
|
387
|
+
"source": "props",
|
|
388
|
+
"propsFieldName": "options",
|
|
389
|
+
"fields": ["value", "label"]
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"componentId": "workout-log-form",
|
|
394
|
+
"componentName": "WorkoutLogForm",
|
|
395
|
+
"isNew": true,
|
|
396
|
+
"atomicType": "organism",
|
|
397
|
+
"composes": ["ui-components-form", "exercise-selector", "ui-components-input", "ui-components-button"],
|
|
398
|
+
"specDeltas": {
|
|
399
|
+
"structure": [
|
|
400
|
+
"Composes Form to create a workout logging form with exercise selector, numeric inputs for sets, reps, weight, and action buttons",
|
|
401
|
+
"Form header region occupies the top with <header> element containing title and instructions",
|
|
402
|
+
"Exercise form region uses <main> with role='form' containing ExerciseSelector and three Input fields for sets, reps, weight, each wrapped in Field with labels",
|
|
403
|
+
"Actions region uses <footer> containing submit Button (variant='default') and cancel Button (variant='outline') in a horizontal row",
|
|
404
|
+
"Each Input is a numeric input with inputMode='numeric' and autocomplete='off'",
|
|
405
|
+
"Uses semantic landmarks: <header> for form-header, <main> for exercise-form, <footer> for actions"
|
|
406
|
+
],
|
|
407
|
+
"rendering": [
|
|
408
|
+
"In loading state (isLoading=true), renders Skeleton placeholders in exercise-form region: one for selector (h-10 w-full), three for inputs (h-10 w-24 each); hides actions region",
|
|
409
|
+
"In ready state, renders all regions: form-header with title 'Log Workout Session' and instructions 'Enter your workout details', exercise-form with empty fields, actions with enabled buttons",
|
|
410
|
+
"In editing-invalid state, renders inline error messages below invalid fields using FormMessage with text-destructive, sets aria-invalid='true' and aria-describedby pointing to the error id on invalid inputs; submit button remains enabled",
|
|
411
|
+
"In submitting state (isSubmitting=true), disables all inputs and buttons, renders Spinner next to submit button with aria-busy='true' and aria-live='polite' announcing 'Saving workout session'",
|
|
412
|
+
"In error state (error is truthy), hides exercise-form and actions regions, renders Alert with destructive variant in form-header region displaying the error message and a retry Button",
|
|
413
|
+
"Renders field-level validation errors for sets/reps/weight: must be positive integers, shows 'Must be a positive number' below input when invalid"
|
|
414
|
+
],
|
|
415
|
+
"interaction": [
|
|
416
|
+
"Calls onSubmit: (data: { exercise: string; sets: number; reps: number; weight: number }) => void when form is valid and submit button is clicked, after validating all fields",
|
|
417
|
+
"Calls onCancel: () => void when cancel button is clicked",
|
|
418
|
+
"On input change for sets/reps/weight, validates value > 0 and integer, sets internal error state if invalid, clears error when user edits the field again",
|
|
419
|
+
"Form submission uses <form> element for native Enter-to-submit on inputs",
|
|
420
|
+
"Submit button disables during submitting state to prevent duplicate submissions",
|
|
421
|
+
"Retry button in error state calls onRetry: () => void, which transitions back to loading state",
|
|
422
|
+
"Validation triggers on blur for each field and on submit for the whole form"
|
|
423
|
+
],
|
|
424
|
+
"styling": [
|
|
425
|
+
"Root form uses `flex flex-col gap-6 p-6 bg-background rounded-lg shadow-md` with `max-w-md mx-auto`.",
|
|
426
|
+
"Header uses `text-2xl font-semibold tracking-tight text-foreground text-balance` for title, `text-sm text-muted-foreground` for instructions, with `space-y-2`.",
|
|
427
|
+
"Main exercise-form uses `flex flex-col gap-4`.",
|
|
428
|
+
"Each Field uses `flex flex-col gap-1`, label `text-sm font-medium text-foreground`, input `h-10 px-3 py-2 border border-input bg-background text-sm text-foreground rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
429
|
+
"Numeric inputs (sets, reps, weight) use `text-right font-variant-numeric: tabular-nums` with `w-24`.",
|
|
430
|
+
"Footer actions uses `flex justify-end gap-4 mt-4`.",
|
|
431
|
+
"Submit button uses `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md font-medium text-sm` with `active:scale-[0.97] transition: transform 100ms ease-out`; hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 transition: background-color 150ms ease }`.",
|
|
432
|
+
"Cancel button uses `border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 rounded-md font-medium text-sm` with `active:scale-[0.97] transition: transform 100ms ease-out`; hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent transition: background-color 150ms ease }`.",
|
|
433
|
+
"In loading state, skeletons use `h-10 w-full bg-muted animate-pulse rounded-md` for selector, `h-10 w-24 bg-muted animate-pulse rounded-md` for inputs.",
|
|
434
|
+
"In editing-invalid state, invalid inputs use `border-destructive focus-visible:ring-destructive`, error messages `text-sm text-destructive`.",
|
|
435
|
+
"In submitting state, disabled elements use `opacity-50 cursor-not-allowed`, spinner uses `h-5 w-5 animate-spin text-primary`.",
|
|
436
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex items-center gap-2`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md`.",
|
|
437
|
+
"Touch targets for buttons are `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
438
|
+
"Focus on inputs uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
439
|
+
"Respects prefers-reduced-motion by using `motion-reduce:transition-none` for CSS transitions."
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
"props": [
|
|
443
|
+
{
|
|
444
|
+
"name": "onSubmit",
|
|
445
|
+
"type": "(data: { exercise: string; sets: number; reps: number; weight: number }) => void",
|
|
446
|
+
"required": true,
|
|
447
|
+
"description": "Callback for form submission.",
|
|
448
|
+
"category": "callback"
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"name": "onCancel",
|
|
452
|
+
"type": "() => void",
|
|
453
|
+
"required": true,
|
|
454
|
+
"description": "Callback for cancel action.",
|
|
455
|
+
"category": "callback"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
"name": "onRetry",
|
|
459
|
+
"type": "() => void",
|
|
460
|
+
"required": false,
|
|
461
|
+
"description": "Callback for retry in error state.",
|
|
462
|
+
"category": "callback"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "isLoading",
|
|
466
|
+
"type": "boolean",
|
|
467
|
+
"required": false,
|
|
468
|
+
"default": "false",
|
|
469
|
+
"description": "Indicates loading state.",
|
|
470
|
+
"category": "state"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"name": "isSubmitting",
|
|
474
|
+
"type": "boolean",
|
|
475
|
+
"required": false,
|
|
476
|
+
"default": "false",
|
|
477
|
+
"description": "Indicates submitting state.",
|
|
478
|
+
"category": "state"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"name": "error",
|
|
482
|
+
"type": "string | null",
|
|
483
|
+
"required": false,
|
|
484
|
+
"default": "null",
|
|
485
|
+
"description": "Error message for error state.",
|
|
486
|
+
"category": "state"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"name": "exerciseOptions",
|
|
490
|
+
"type": "Array<{ value: string; label: string }>",
|
|
491
|
+
"required": true,
|
|
492
|
+
"description": "Options for exercise selector.",
|
|
493
|
+
"category": "data"
|
|
494
|
+
}
|
|
495
|
+
],
|
|
496
|
+
"storyVariants": [
|
|
497
|
+
{
|
|
498
|
+
"name": "Default",
|
|
499
|
+
"description": "Ready state.",
|
|
500
|
+
"args": {
|
|
501
|
+
"onSubmit": "fn()",
|
|
502
|
+
"onCancel": "fn()",
|
|
503
|
+
"onRetry": "fn()",
|
|
504
|
+
"isLoading": false,
|
|
505
|
+
"isSubmitting": false,
|
|
506
|
+
"error": null,
|
|
507
|
+
"exerciseOptions": [
|
|
508
|
+
{
|
|
509
|
+
"value": "bench-press",
|
|
510
|
+
"label": "Bench Press"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"value": "squat",
|
|
514
|
+
"label": "Squat"
|
|
515
|
+
}
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
"needsPlayFunction": false
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"name": "Loading",
|
|
522
|
+
"description": "Loading state.",
|
|
523
|
+
"args": {
|
|
524
|
+
"onSubmit": "fn()",
|
|
525
|
+
"onCancel": "fn()",
|
|
526
|
+
"onRetry": "fn()",
|
|
527
|
+
"isLoading": true,
|
|
528
|
+
"isSubmitting": false,
|
|
529
|
+
"error": null,
|
|
530
|
+
"exerciseOptions": [
|
|
531
|
+
{
|
|
532
|
+
"value": "bench-press",
|
|
533
|
+
"label": "Bench Press"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"value": "squat",
|
|
537
|
+
"label": "Squat"
|
|
538
|
+
}
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
"needsPlayFunction": false
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"name": "EditingInvalid",
|
|
545
|
+
"description": "Editing with validation errors.",
|
|
546
|
+
"args": {
|
|
547
|
+
"onSubmit": "fn()",
|
|
548
|
+
"onCancel": "fn()",
|
|
549
|
+
"onRetry": "fn()",
|
|
550
|
+
"isLoading": false,
|
|
551
|
+
"isSubmitting": false,
|
|
552
|
+
"error": null,
|
|
553
|
+
"exerciseOptions": [
|
|
554
|
+
{
|
|
555
|
+
"value": "bench-press",
|
|
556
|
+
"label": "Bench Press"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"value": "squat",
|
|
560
|
+
"label": "Squat"
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
},
|
|
564
|
+
"needsPlayFunction": true,
|
|
565
|
+
"playDescription": "1. userEvent.type(getByLabelText('Sets'), '-1'), 2. userEvent.tab(), 3. await waitFor(() => expect(getByText('Must be a positive number')).toBeVisible()), 4. userEvent.type(getByLabelText('Sets'), '3'), 5. await waitFor(() => expect(queryByText('Must be a positive number')).not.toBeInTheDocument())"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"name": "Submitting",
|
|
569
|
+
"description": "Submitting state.",
|
|
570
|
+
"args": {
|
|
571
|
+
"onSubmit": "fn()",
|
|
572
|
+
"onCancel": "fn()",
|
|
573
|
+
"onRetry": "fn()",
|
|
574
|
+
"isLoading": false,
|
|
575
|
+
"isSubmitting": true,
|
|
576
|
+
"error": null,
|
|
577
|
+
"exerciseOptions": [
|
|
578
|
+
{
|
|
579
|
+
"value": "bench-press",
|
|
580
|
+
"label": "Bench Press"
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"value": "squat",
|
|
584
|
+
"label": "Squat"
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
},
|
|
588
|
+
"needsPlayFunction": false
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
"name": "Error",
|
|
592
|
+
"description": "Error state.",
|
|
593
|
+
"args": {
|
|
594
|
+
"onSubmit": "fn()",
|
|
595
|
+
"onCancel": "fn()",
|
|
596
|
+
"onRetry": "fn()",
|
|
597
|
+
"isLoading": false,
|
|
598
|
+
"isSubmitting": false,
|
|
599
|
+
"error": "Failed to save",
|
|
600
|
+
"exerciseOptions": [
|
|
601
|
+
{
|
|
602
|
+
"value": "bench-press",
|
|
603
|
+
"label": "Bench Press"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"value": "squat",
|
|
607
|
+
"label": "Squat"
|
|
608
|
+
}
|
|
609
|
+
]
|
|
610
|
+
},
|
|
611
|
+
"needsPlayFunction": true,
|
|
612
|
+
"playDescription": "1. userEvent.click(getByRole('button', { name: 'Retry' })), 2. expect(onRetry).toHaveBeenCalled()"
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
"dataContract": {
|
|
616
|
+
"source": "local-state",
|
|
617
|
+
"fields": ["exercise", "sets", "reps", "weight"]
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"componentId": "strength-progress-dashboard",
|
|
622
|
+
"componentName": "StrengthProgressDashboard",
|
|
623
|
+
"isNew": true,
|
|
624
|
+
"atomicType": "organism",
|
|
625
|
+
"composes": ["ui-components-chart", "exercise-selector", "time-period-selector", "ui-components-button"],
|
|
626
|
+
"specDeltas": {
|
|
627
|
+
"structure": [
|
|
628
|
+
"Composes Chart for displaying progress, with ExerciseSelector and TimePeriodSelector in a horizontal row above, and actions in a footer row",
|
|
629
|
+
"Progress-charts region uses <main> with role='region' and aria-label='Progress Charts' containing the Chart",
|
|
630
|
+
"Exercise-selector and time-period-selector regions are <aside> elements in a flex row above the chart",
|
|
631
|
+
"Actions region uses <footer> containing 'Log Workout' (default variant) and 'View Leaderboards' (outline variant) buttons",
|
|
632
|
+
"Uses semantic landmarks: <main> for progress-charts, <aside> for selectors, <footer> for actions"
|
|
633
|
+
],
|
|
634
|
+
"rendering": [
|
|
635
|
+
"In loading state (isLoading=true), renders Skeleton in progress-charts region (h-64 w-full), hides selectors and actions",
|
|
636
|
+
"In ready state, renders all regions: Chart with data from progressData, selectors with current values, enabled action buttons",
|
|
637
|
+
"In error state (error is truthy), hides selectors and actions, renders Alert with destructive variant in progress-charts region displaying error message and retry Button",
|
|
638
|
+
"Renders Chart as a line chart showing strength progress over time for the selected exercise and period"
|
|
639
|
+
],
|
|
640
|
+
"interaction": [
|
|
641
|
+
"Calls onExerciseChange: (exercise: string) => void when exercise selector changes, triggering reload",
|
|
642
|
+
"Calls onPeriodChange: (period: string) => void when time period selector changes, triggering reload",
|
|
643
|
+
"Calls onLogWorkout: () => void when 'Log Workout' button is clicked",
|
|
644
|
+
"Calls onViewLeaderboards: () => void when 'View Leaderboards' button is clicked",
|
|
645
|
+
"Retry button in error state calls onRetry: () => void, transitioning to loading state"
|
|
646
|
+
],
|
|
647
|
+
"styling": [
|
|
648
|
+
"Root container uses `flex flex-col gap-6 p-6 bg-background`.",
|
|
649
|
+
"Selectors row uses `flex flex-row gap-4 justify-start`.",
|
|
650
|
+
"Main progress-charts uses `h-64 w-full bg-card rounded-lg shadow-sm` with `overflow-hidden`.",
|
|
651
|
+
"Chart uses `w-full h-full` with CSS variables for colors, responsive sizing via `ResponsiveContainer`.",
|
|
652
|
+
"Footer actions uses `flex justify-end gap-4`.",
|
|
653
|
+
"Log Workout button uses `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md font-medium text-sm` with `active:scale-[0.97] transition: transform 100ms ease-out`; hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 transition: background-color 150ms ease }`.",
|
|
654
|
+
"View Leaderboards button uses `border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 rounded-md font-medium text-sm` with `active:scale-[0.97] transition: transform 100ms ease-out`; hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent transition: background-color 150ms ease }`.",
|
|
655
|
+
"In loading state, skeleton uses `h-64 w-full bg-muted animate-pulse rounded-lg`.",
|
|
656
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex items-center gap-2`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md`.",
|
|
657
|
+
"Chart lines use `stroke: var(--primary)` with `stroke-width: 2`, points `fill: var(--primary)`.",
|
|
658
|
+
"Touch targets for buttons are `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
659
|
+
"Numeric values in chart use `font-variant-numeric: tabular-nums`.",
|
|
660
|
+
"Headings use `text-balance`."
|
|
661
|
+
]
|
|
662
|
+
},
|
|
663
|
+
"props": [
|
|
664
|
+
{
|
|
665
|
+
"name": "progressData",
|
|
666
|
+
"type": "Array<{ date: string; value: number }>",
|
|
667
|
+
"required": true,
|
|
668
|
+
"description": "Data for the progress chart.",
|
|
669
|
+
"category": "data"
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
"name": "exerciseOptions",
|
|
673
|
+
"type": "Array<{ value: string; label: string }>",
|
|
674
|
+
"required": true,
|
|
675
|
+
"description": "Options for exercise selector.",
|
|
676
|
+
"category": "data"
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
"name": "periodOptions",
|
|
680
|
+
"type": "Array<{ value: string; label: string }>",
|
|
681
|
+
"required": true,
|
|
682
|
+
"description": "Options for time period selector.",
|
|
683
|
+
"category": "data"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
"name": "selectedExercise",
|
|
687
|
+
"type": "string",
|
|
688
|
+
"required": false,
|
|
689
|
+
"default": "''",
|
|
690
|
+
"description": "Currently selected exercise.",
|
|
691
|
+
"category": "state"
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"name": "selectedPeriod",
|
|
695
|
+
"type": "string",
|
|
696
|
+
"required": false,
|
|
697
|
+
"default": "''",
|
|
698
|
+
"description": "Currently selected period.",
|
|
699
|
+
"category": "state"
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
"name": "onExerciseChange",
|
|
703
|
+
"type": "(exercise: string) => void",
|
|
704
|
+
"required": true,
|
|
705
|
+
"description": "Callback for exercise change.",
|
|
706
|
+
"category": "callback"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
"name": "onPeriodChange",
|
|
710
|
+
"type": "(period: string) => void",
|
|
711
|
+
"required": true,
|
|
712
|
+
"description": "Callback for period change.",
|
|
713
|
+
"category": "callback"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"name": "onLogWorkout",
|
|
717
|
+
"type": "() => void",
|
|
718
|
+
"required": true,
|
|
719
|
+
"description": "Callback for log workout action.",
|
|
720
|
+
"category": "callback"
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
"name": "onViewLeaderboards",
|
|
724
|
+
"type": "() => void",
|
|
725
|
+
"required": true,
|
|
726
|
+
"description": "Callback for view leaderboards action.",
|
|
727
|
+
"category": "callback"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"name": "onRetry",
|
|
731
|
+
"type": "() => void",
|
|
732
|
+
"required": false,
|
|
733
|
+
"description": "Callback for retry in error state.",
|
|
734
|
+
"category": "callback"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
"name": "isLoading",
|
|
738
|
+
"type": "boolean",
|
|
739
|
+
"required": false,
|
|
740
|
+
"default": "false",
|
|
741
|
+
"description": "Indicates loading state.",
|
|
742
|
+
"category": "state"
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
"name": "error",
|
|
746
|
+
"type": "string | null",
|
|
747
|
+
"required": false,
|
|
748
|
+
"default": "null",
|
|
749
|
+
"description": "Error message for error state.",
|
|
750
|
+
"category": "state"
|
|
751
|
+
}
|
|
752
|
+
],
|
|
753
|
+
"storyVariants": [
|
|
754
|
+
{
|
|
755
|
+
"name": "Default",
|
|
756
|
+
"description": "Ready state with data.",
|
|
757
|
+
"args": {
|
|
758
|
+
"progressData": [
|
|
759
|
+
{
|
|
760
|
+
"date": "2024-01-01",
|
|
761
|
+
"value": 100
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
"date": "2024-02-01",
|
|
765
|
+
"value": 120
|
|
766
|
+
}
|
|
767
|
+
],
|
|
768
|
+
"exerciseOptions": [
|
|
769
|
+
{
|
|
770
|
+
"value": "bench-press",
|
|
771
|
+
"label": "Bench Press"
|
|
772
|
+
}
|
|
773
|
+
],
|
|
774
|
+
"periodOptions": [
|
|
775
|
+
{
|
|
776
|
+
"value": "month",
|
|
777
|
+
"label": "Last Month"
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
"selectedExercise": "bench-press",
|
|
781
|
+
"selectedPeriod": "month",
|
|
782
|
+
"onExerciseChange": "fn()",
|
|
783
|
+
"onPeriodChange": "fn()",
|
|
784
|
+
"onLogWorkout": "fn()",
|
|
785
|
+
"onViewLeaderboards": "fn()",
|
|
786
|
+
"onRetry": "fn()",
|
|
787
|
+
"isLoading": false,
|
|
788
|
+
"error": null
|
|
789
|
+
},
|
|
790
|
+
"needsPlayFunction": false
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"name": "Loading",
|
|
794
|
+
"description": "Loading state.",
|
|
795
|
+
"args": {
|
|
796
|
+
"progressData": [
|
|
797
|
+
{
|
|
798
|
+
"date": "2024-01-01",
|
|
799
|
+
"value": 100
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"date": "2024-02-01",
|
|
803
|
+
"value": 120
|
|
804
|
+
}
|
|
805
|
+
],
|
|
806
|
+
"exerciseOptions": [
|
|
807
|
+
{
|
|
808
|
+
"value": "bench-press",
|
|
809
|
+
"label": "Bench Press"
|
|
810
|
+
}
|
|
811
|
+
],
|
|
812
|
+
"periodOptions": [
|
|
813
|
+
{
|
|
814
|
+
"value": "month",
|
|
815
|
+
"label": "Last Month"
|
|
816
|
+
}
|
|
817
|
+
],
|
|
818
|
+
"selectedExercise": "bench-press",
|
|
819
|
+
"selectedPeriod": "month",
|
|
820
|
+
"onExerciseChange": "fn()",
|
|
821
|
+
"onPeriodChange": "fn()",
|
|
822
|
+
"onLogWorkout": "fn()",
|
|
823
|
+
"onViewLeaderboards": "fn()",
|
|
824
|
+
"onRetry": "fn()",
|
|
825
|
+
"isLoading": true,
|
|
826
|
+
"error": null
|
|
827
|
+
},
|
|
828
|
+
"needsPlayFunction": false
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"name": "Error",
|
|
832
|
+
"description": "Error state.",
|
|
833
|
+
"args": {
|
|
834
|
+
"progressData": [
|
|
835
|
+
{
|
|
836
|
+
"date": "2024-01-01",
|
|
837
|
+
"value": 100
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"date": "2024-02-01",
|
|
841
|
+
"value": 120
|
|
842
|
+
}
|
|
843
|
+
],
|
|
844
|
+
"exerciseOptions": [
|
|
845
|
+
{
|
|
846
|
+
"value": "bench-press",
|
|
847
|
+
"label": "Bench Press"
|
|
848
|
+
}
|
|
849
|
+
],
|
|
850
|
+
"periodOptions": [
|
|
851
|
+
{
|
|
852
|
+
"value": "month",
|
|
853
|
+
"label": "Last Month"
|
|
854
|
+
}
|
|
855
|
+
],
|
|
856
|
+
"selectedExercise": "bench-press",
|
|
857
|
+
"selectedPeriod": "month",
|
|
858
|
+
"onExerciseChange": "fn()",
|
|
859
|
+
"onPeriodChange": "fn()",
|
|
860
|
+
"onLogWorkout": "fn()",
|
|
861
|
+
"onViewLeaderboards": "fn()",
|
|
862
|
+
"onRetry": "fn()",
|
|
863
|
+
"isLoading": false,
|
|
864
|
+
"error": "Failed to load data"
|
|
865
|
+
},
|
|
866
|
+
"needsPlayFunction": true,
|
|
867
|
+
"playDescription": "1. userEvent.click(getByRole('button', { name: 'Retry' })), 2. expect(onRetry).toHaveBeenCalled()"
|
|
868
|
+
}
|
|
869
|
+
],
|
|
870
|
+
"dataContract": {
|
|
871
|
+
"source": "props",
|
|
872
|
+
"propsFieldName": "progressData",
|
|
873
|
+
"fields": ["date", "value"]
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
"componentId": "leaderboard",
|
|
878
|
+
"componentName": "Leaderboard",
|
|
879
|
+
"isNew": true,
|
|
880
|
+
"atomicType": "organism",
|
|
881
|
+
"composes": ["ui-components-table", "exercise-selector", "time-period-selector", "ui-components-badge"],
|
|
882
|
+
"specDeltas": {
|
|
883
|
+
"structure": [
|
|
884
|
+
"Composes Table for ranked list, with ExerciseSelector and TimePeriodSelector in a flex row above, and user-rank as a highlighted TableRow",
|
|
885
|
+
"Leaderboard-list region uses <main> with role='region' and aria-label='Leaderboard List' containing the Table",
|
|
886
|
+
"Filters region is <aside> with selectors in a horizontal row",
|
|
887
|
+
"User-rank region is a <section> with role='region' and aria-label='Your Rank' showing Badge with rank number and position text",
|
|
888
|
+
"Table structure: TableHeader with columns for rank, athlete, performance; TableBody with TableRow for each entry"
|
|
889
|
+
],
|
|
890
|
+
"rendering": [
|
|
891
|
+
"In loading state (isLoading=true), renders Skeleton rows in leaderboard-list region (5 rows, each with h-10 w-full)",
|
|
892
|
+
"In ready state, renders Table with leaderboardData, highlights user's row with accent background if present",
|
|
893
|
+
"In error state (error is truthy), renders Alert with destructive variant in leaderboard-list region displaying error message and retry Button, hides filters and user-rank",
|
|
894
|
+
"Renders user rank as a Badge with variant='default' showing 'Rank #X' where X is from userRank prop"
|
|
895
|
+
],
|
|
896
|
+
"interaction": [
|
|
897
|
+
"Calls onExerciseChange: (exercise: string) => void when exercise selector changes",
|
|
898
|
+
"Calls onPeriodChange: (period: string) => void when time period selector changes",
|
|
899
|
+
"Calls onViewProgress: () => void when 'View My Progress' button is clicked (secondary emphasis)",
|
|
900
|
+
"Retry button calls onRetry: () => void, transitioning to loading"
|
|
901
|
+
],
|
|
902
|
+
"styling": [
|
|
903
|
+
"Root container uses `flex flex-col gap-6 p-6 bg-background`.",
|
|
904
|
+
"Filters row uses `flex flex-row gap-4 justify-start`.",
|
|
905
|
+
"Main leaderboard-list uses `overflow-auto border border-border rounded-md shadow-sm`.",
|
|
906
|
+
"Table uses `w-full border-collapse`, rows `border-b last:border-b-0 hover:bg-muted/50` with `@media (hover: hover) and (pointer: fine) { hover:bg-muted/50 transition: background-color 150ms ease }`.",
|
|
907
|
+
"TableHead uses `text-left font-medium text-muted-foreground text-sm p-4 bg-muted/50`.",
|
|
908
|
+
"TableCell uses `p-4 text-sm text-foreground`, rank `font-bold tabular-nums`, performance `text-right tabular-nums`.",
|
|
909
|
+
"User row uses `bg-accent text-accent-foreground font-semibold`.",
|
|
910
|
+
"User-rank section uses `flex items-center gap-2 p-4 bg-card rounded-md shadow-sm`, badge `bg-primary text-primary-foreground px-2 py-1 rounded-full text-sm font-medium`.",
|
|
911
|
+
"In loading state, skeleton rows use `h-10 w-full bg-muted animate-pulse` for 5 rows.",
|
|
912
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex items-center gap-2`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md`.",
|
|
913
|
+
"Touch targets for interactive elements are `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
914
|
+
"Focus on filters uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
915
|
+
"View My Progress button uses `border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 rounded-md font-medium text-sm` with `active:scale-[0.97] transition: transform 100ms ease-out`; hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent transition: background-color 150ms ease }`."
|
|
916
|
+
]
|
|
917
|
+
},
|
|
918
|
+
"props": [
|
|
919
|
+
{
|
|
920
|
+
"name": "leaderboardData",
|
|
921
|
+
"type": "Array<{ rank: number; athlete: string; performance: number }>",
|
|
922
|
+
"required": true,
|
|
923
|
+
"description": "Data for the leaderboard table.",
|
|
924
|
+
"category": "data"
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
"name": "exerciseOptions",
|
|
928
|
+
"type": "Array<{ value: string; label: string }>",
|
|
929
|
+
"required": true,
|
|
930
|
+
"description": "Options for exercise filter.",
|
|
931
|
+
"category": "data"
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
"name": "periodOptions",
|
|
935
|
+
"type": "Array<{ value: string; label: string }>",
|
|
936
|
+
"required": true,
|
|
937
|
+
"description": "Options for time period filter.",
|
|
938
|
+
"category": "data"
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
"name": "selectedExercise",
|
|
942
|
+
"type": "string",
|
|
943
|
+
"required": false,
|
|
944
|
+
"default": "''",
|
|
945
|
+
"description": "Selected exercise.",
|
|
946
|
+
"category": "state"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
"name": "selectedPeriod",
|
|
950
|
+
"type": "string",
|
|
951
|
+
"required": false,
|
|
952
|
+
"default": "''",
|
|
953
|
+
"description": "Selected period.",
|
|
954
|
+
"category": "state"
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"name": "userRank",
|
|
958
|
+
"type": "number | null",
|
|
959
|
+
"required": false,
|
|
960
|
+
"default": "null",
|
|
961
|
+
"description": "User's current rank.",
|
|
962
|
+
"category": "data"
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
"name": "onExerciseChange",
|
|
966
|
+
"type": "(exercise: string) => void",
|
|
967
|
+
"required": true,
|
|
968
|
+
"description": "Callback for exercise change.",
|
|
969
|
+
"category": "callback"
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
"name": "onPeriodChange",
|
|
973
|
+
"type": "(period: string) => void",
|
|
974
|
+
"required": true,
|
|
975
|
+
"description": "Callback for period change.",
|
|
976
|
+
"category": "callback"
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"name": "onViewProgress",
|
|
980
|
+
"type": "() => void",
|
|
981
|
+
"required": true,
|
|
982
|
+
"description": "Callback for view progress action.",
|
|
983
|
+
"category": "callback"
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"name": "onRetry",
|
|
987
|
+
"type": "() => void",
|
|
988
|
+
"required": false,
|
|
989
|
+
"description": "Callback for retry.",
|
|
990
|
+
"category": "callback"
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
"name": "isLoading",
|
|
994
|
+
"type": "boolean",
|
|
995
|
+
"required": false,
|
|
996
|
+
"default": "false",
|
|
997
|
+
"description": "Loading state.",
|
|
998
|
+
"category": "state"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
"name": "error",
|
|
1002
|
+
"type": "string | null",
|
|
1003
|
+
"required": false,
|
|
1004
|
+
"default": "null",
|
|
1005
|
+
"description": "Error message.",
|
|
1006
|
+
"category": "state"
|
|
1007
|
+
}
|
|
1008
|
+
],
|
|
1009
|
+
"storyVariants": [
|
|
1010
|
+
{
|
|
1011
|
+
"name": "Default",
|
|
1012
|
+
"description": "Ready state with data.",
|
|
1013
|
+
"args": {
|
|
1014
|
+
"leaderboardData": [
|
|
1015
|
+
{
|
|
1016
|
+
"rank": 1,
|
|
1017
|
+
"athlete": "John Doe",
|
|
1018
|
+
"performance": 200
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
"rank": 2,
|
|
1022
|
+
"athlete": "Jane Smith",
|
|
1023
|
+
"performance": 180
|
|
1024
|
+
}
|
|
1025
|
+
],
|
|
1026
|
+
"exerciseOptions": [
|
|
1027
|
+
{
|
|
1028
|
+
"value": "bench-press",
|
|
1029
|
+
"label": "Bench Press"
|
|
1030
|
+
}
|
|
1031
|
+
],
|
|
1032
|
+
"periodOptions": [
|
|
1033
|
+
{
|
|
1034
|
+
"value": "month",
|
|
1035
|
+
"label": "Last Month"
|
|
1036
|
+
}
|
|
1037
|
+
],
|
|
1038
|
+
"selectedExercise": "bench-press",
|
|
1039
|
+
"selectedPeriod": "month",
|
|
1040
|
+
"userRank": 3,
|
|
1041
|
+
"onExerciseChange": "fn()",
|
|
1042
|
+
"onPeriodChange": "fn()",
|
|
1043
|
+
"onViewProgress": "fn()",
|
|
1044
|
+
"onRetry": "fn()",
|
|
1045
|
+
"isLoading": false,
|
|
1046
|
+
"error": null
|
|
1047
|
+
},
|
|
1048
|
+
"needsPlayFunction": false
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
"name": "Loading",
|
|
1052
|
+
"description": "Loading state.",
|
|
1053
|
+
"args": {
|
|
1054
|
+
"leaderboardData": [
|
|
1055
|
+
{
|
|
1056
|
+
"rank": 1,
|
|
1057
|
+
"athlete": "John Doe",
|
|
1058
|
+
"performance": 200
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
"rank": 2,
|
|
1062
|
+
"athlete": "Jane Smith",
|
|
1063
|
+
"performance": 180
|
|
1064
|
+
}
|
|
1065
|
+
],
|
|
1066
|
+
"exerciseOptions": [
|
|
1067
|
+
{
|
|
1068
|
+
"value": "bench-press",
|
|
1069
|
+
"label": "Bench Press"
|
|
1070
|
+
}
|
|
1071
|
+
],
|
|
1072
|
+
"periodOptions": [
|
|
1073
|
+
{
|
|
1074
|
+
"value": "month",
|
|
1075
|
+
"label": "Last Month"
|
|
1076
|
+
}
|
|
1077
|
+
],
|
|
1078
|
+
"selectedExercise": "bench-press",
|
|
1079
|
+
"selectedPeriod": "month",
|
|
1080
|
+
"userRank": 3,
|
|
1081
|
+
"onExerciseChange": "fn()",
|
|
1082
|
+
"onPeriodChange": "fn()",
|
|
1083
|
+
"onViewProgress": "fn()",
|
|
1084
|
+
"onRetry": "fn()",
|
|
1085
|
+
"isLoading": true,
|
|
1086
|
+
"error": null
|
|
1087
|
+
},
|
|
1088
|
+
"needsPlayFunction": false
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
"name": "Error",
|
|
1092
|
+
"description": "Error state.",
|
|
1093
|
+
"args": {
|
|
1094
|
+
"leaderboardData": [
|
|
1095
|
+
{
|
|
1096
|
+
"rank": 1,
|
|
1097
|
+
"athlete": "John Doe",
|
|
1098
|
+
"performance": 200
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
"rank": 2,
|
|
1102
|
+
"athlete": "Jane Smith",
|
|
1103
|
+
"performance": 180
|
|
1104
|
+
}
|
|
1105
|
+
],
|
|
1106
|
+
"exerciseOptions": [
|
|
1107
|
+
{
|
|
1108
|
+
"value": "bench-press",
|
|
1109
|
+
"label": "Bench Press"
|
|
1110
|
+
}
|
|
1111
|
+
],
|
|
1112
|
+
"periodOptions": [
|
|
1113
|
+
{
|
|
1114
|
+
"value": "month",
|
|
1115
|
+
"label": "Last Month"
|
|
1116
|
+
}
|
|
1117
|
+
],
|
|
1118
|
+
"selectedExercise": "bench-press",
|
|
1119
|
+
"selectedPeriod": "month",
|
|
1120
|
+
"userRank": 3,
|
|
1121
|
+
"onExerciseChange": "fn()",
|
|
1122
|
+
"onPeriodChange": "fn()",
|
|
1123
|
+
"onViewProgress": "fn()",
|
|
1124
|
+
"onRetry": "fn()",
|
|
1125
|
+
"isLoading": false,
|
|
1126
|
+
"error": "Failed to load leaderboard"
|
|
1127
|
+
},
|
|
1128
|
+
"needsPlayFunction": true,
|
|
1129
|
+
"playDescription": "1. userEvent.click(getByRole('button', { name: 'Retry' })), 2. expect(onRetry).toHaveBeenCalled()"
|
|
1130
|
+
}
|
|
1131
|
+
],
|
|
1132
|
+
"dataContract": {
|
|
1133
|
+
"source": "props",
|
|
1134
|
+
"propsFieldName": "leaderboardData",
|
|
1135
|
+
"fields": ["rank", "athlete", "performance"]
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
"componentId": "log-workout-session-page",
|
|
1140
|
+
"componentName": "LogWorkoutSessionPage",
|
|
1141
|
+
"isNew": true,
|
|
1142
|
+
"atomicType": "page",
|
|
1143
|
+
"composes": ["workout-log-form"],
|
|
1144
|
+
"specDeltas": {
|
|
1145
|
+
"structure": [
|
|
1146
|
+
"Page-level component with dedicated route '/log-workout-session'",
|
|
1147
|
+
"Composes WorkoutLogForm in the main content area",
|
|
1148
|
+
"Uses <main> for the form container with role='main'"
|
|
1149
|
+
],
|
|
1150
|
+
"rendering": [
|
|
1151
|
+
"In loading state, shows WorkoutLogForm with isLoading=true, rendering skeletons for form fields",
|
|
1152
|
+
"In ready state, shows WorkoutLogForm with empty fields ready for input",
|
|
1153
|
+
"In editing-invalid state, shows WorkoutLogForm with validation errors displayed inline",
|
|
1154
|
+
"In submitting state, shows WorkoutLogForm with isSubmitting=true, disabling inputs and showing progress spinner",
|
|
1155
|
+
"In error state, shows WorkoutLogForm with error='Failed to save workout session', rendering error banner with retry button; hides form fields"
|
|
1156
|
+
],
|
|
1157
|
+
"interaction": [
|
|
1158
|
+
"On submit, calls mutation to save workout data, transitions to submitting, on success navigates to view-strength-progress, on failure to error",
|
|
1159
|
+
"On cancel, navigates to view-strength-progress without saving",
|
|
1160
|
+
"On retry from error, re-attempts the submission or reloads form data"
|
|
1161
|
+
],
|
|
1162
|
+
"styling": [
|
|
1163
|
+
"Root page uses `min-h-screen bg-background flex items-center justify-center p-4`.",
|
|
1164
|
+
"Main container uses `w-full max-w-md` with `useSpring({ opacity: isLoading ? 0 : 1, config: { tension: 400, friction: 26 } })` for subtle fade on load; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
1165
|
+
"In loading state, skeletons use `h-10 w-full bg-muted animate-pulse rounded-md` matching form structure.",
|
|
1166
|
+
"In submitting state, progress spinner uses `h-5 w-5 animate-spin text-primary` with CSS `@keyframes spin { 0% { transform: rotate(0deg) } 100% { transform: rotate(360deg) } }` and `animation: spin 1s linear infinite`.",
|
|
1167
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex flex-col gap-2 items-center`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md` with hover animation.",
|
|
1168
|
+
"Page title (if present) uses `text-3xl font-bold tracking-tight text-foreground text-balance mb-6`.",
|
|
1169
|
+
"Transitions between states use `useTransition(state, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: { tension: 300, friction: 22 } })`; respects reduced motion.",
|
|
1170
|
+
"All interactive elements have `min-h-11 min-w-11 touch-action: manipulation`.",
|
|
1171
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
1172
|
+
"Numeric displays use `font-variant-numeric: tabular-nums`."
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
"props": [
|
|
1176
|
+
{
|
|
1177
|
+
"name": "isLoading",
|
|
1178
|
+
"type": "boolean",
|
|
1179
|
+
"required": false,
|
|
1180
|
+
"default": "false",
|
|
1181
|
+
"description": "Controls loading state.",
|
|
1182
|
+
"category": "state"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"name": "isSubmitting",
|
|
1186
|
+
"type": "boolean",
|
|
1187
|
+
"required": false,
|
|
1188
|
+
"default": "false",
|
|
1189
|
+
"description": "Controls submitting state.",
|
|
1190
|
+
"category": "state"
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
"name": "error",
|
|
1194
|
+
"type": "string | null",
|
|
1195
|
+
"required": false,
|
|
1196
|
+
"default": "null",
|
|
1197
|
+
"description": "Controls error state.",
|
|
1198
|
+
"category": "state"
|
|
1199
|
+
}
|
|
1200
|
+
],
|
|
1201
|
+
"storyVariants": [
|
|
1202
|
+
{
|
|
1203
|
+
"name": "Default",
|
|
1204
|
+
"description": "Ready state.",
|
|
1205
|
+
"args": {
|
|
1206
|
+
"isLoading": false,
|
|
1207
|
+
"isSubmitting": false,
|
|
1208
|
+
"error": null
|
|
1209
|
+
},
|
|
1210
|
+
"needsPlayFunction": false
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
"name": "Loading",
|
|
1214
|
+
"description": "Loading state.",
|
|
1215
|
+
"args": {
|
|
1216
|
+
"isLoading": true,
|
|
1217
|
+
"isSubmitting": false,
|
|
1218
|
+
"error": null
|
|
1219
|
+
},
|
|
1220
|
+
"needsPlayFunction": false
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"name": "Submitting",
|
|
1224
|
+
"description": "Submitting state.",
|
|
1225
|
+
"args": {
|
|
1226
|
+
"isLoading": false,
|
|
1227
|
+
"isSubmitting": true,
|
|
1228
|
+
"error": null
|
|
1229
|
+
},
|
|
1230
|
+
"needsPlayFunction": false
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
"name": "Error",
|
|
1234
|
+
"description": "Error state.",
|
|
1235
|
+
"args": {
|
|
1236
|
+
"isLoading": false,
|
|
1237
|
+
"isSubmitting": false,
|
|
1238
|
+
"error": "Failed to save"
|
|
1239
|
+
},
|
|
1240
|
+
"needsPlayFunction": false
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
"name": "InteractiveSubmit",
|
|
1244
|
+
"description": "Submitting the form.",
|
|
1245
|
+
"args": {
|
|
1246
|
+
"isLoading": false,
|
|
1247
|
+
"isSubmitting": false,
|
|
1248
|
+
"error": null
|
|
1249
|
+
},
|
|
1250
|
+
"needsPlayFunction": true,
|
|
1251
|
+
"playDescription": "1. userEvent.type(getByLabelText('Exercise'), 'Bench Press'), 2. userEvent.type(getByLabelText('Sets'), '3'), 3. userEvent.type(getByLabelText('Reps'), '10'), 4. userEvent.type(getByLabelText('Weight'), '100'), 5. userEvent.click(getByRole('button', { name: 'Log Workout' })), 6. await waitFor(() => expect(getByText('Saving workout session')).toBeVisible())"
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
"dataContract": {
|
|
1255
|
+
"source": "graphql-mutation",
|
|
1256
|
+
"operationName": "LogWorkoutSession",
|
|
1257
|
+
"fields": ["exercise", "sets", "reps", "weight"]
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
"componentId": "view-strength-progress-page",
|
|
1262
|
+
"componentName": "ViewStrengthProgressPage",
|
|
1263
|
+
"isNew": true,
|
|
1264
|
+
"atomicType": "page",
|
|
1265
|
+
"composes": ["strength-progress-dashboard"],
|
|
1266
|
+
"specDeltas": {
|
|
1267
|
+
"structure": [
|
|
1268
|
+
"Page-level component with dedicated route '/view-strength-progress'",
|
|
1269
|
+
"Composes StrengthProgressDashboard in the main content area",
|
|
1270
|
+
"Uses <main> for the dashboard container with role='main'"
|
|
1271
|
+
],
|
|
1272
|
+
"rendering": [
|
|
1273
|
+
"In loading state, shows StrengthProgressDashboard with isLoading=true, rendering skeleton for chart",
|
|
1274
|
+
"In ready state, shows StrengthProgressDashboard with fetched progressData",
|
|
1275
|
+
"In error state, shows StrengthProgressDashboard with error='Failed to load strength progress data', rendering error banner with retry"
|
|
1276
|
+
],
|
|
1277
|
+
"interaction": [
|
|
1278
|
+
"On page load, fetches progress data via query, transitions to loading, on success to ready, on failure to error",
|
|
1279
|
+
"On filter change (exercise or period), refetches data, transitions to loading",
|
|
1280
|
+
"On log workout, navigates to log-workout-session",
|
|
1281
|
+
"On view leaderboards, navigates to compete-on-leaderboards",
|
|
1282
|
+
"On retry from error, refetches data"
|
|
1283
|
+
],
|
|
1284
|
+
"styling": [
|
|
1285
|
+
"Root page uses `min-h-screen bg-background p-4`.",
|
|
1286
|
+
"Main container uses `max-w-7xl mx-auto` with `useSpring({ opacity: isLoading ? 0 : 1, config: { tension: 400, friction: 26 } })` for fade; respects `useReducedMotion()`.",
|
|
1287
|
+
"In loading state, chart skeleton uses `h-64 w-full bg-muted animate-pulse rounded-lg`.",
|
|
1288
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex flex-col gap-2 items-center`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md` with hover.",
|
|
1289
|
+
"Page title uses `text-3xl font-bold tracking-tight text-foreground text-balance mb-6`.",
|
|
1290
|
+
"State transitions use `useTransition(state, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: { tension: 300, friction: 22 } })`; respects reduced motion.",
|
|
1291
|
+
"Interactive buttons have `min-h-11 min-w-11 touch-action: manipulation`.",
|
|
1292
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
1293
|
+
"Chart numeric labels use `font-variant-numeric: tabular-nums`.",
|
|
1294
|
+
"Headings use `text-balance`.",
|
|
1295
|
+
"Z-index for overlays (if any) uses `z-[400]` for modals."
|
|
1296
|
+
]
|
|
1297
|
+
},
|
|
1298
|
+
"props": [
|
|
1299
|
+
{
|
|
1300
|
+
"name": "isLoading",
|
|
1301
|
+
"type": "boolean",
|
|
1302
|
+
"required": false,
|
|
1303
|
+
"default": "false",
|
|
1304
|
+
"description": "Controls loading state.",
|
|
1305
|
+
"category": "state"
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
"name": "error",
|
|
1309
|
+
"type": "string | null",
|
|
1310
|
+
"required": false,
|
|
1311
|
+
"default": "null",
|
|
1312
|
+
"description": "Controls error state.",
|
|
1313
|
+
"category": "state"
|
|
1314
|
+
}
|
|
1315
|
+
],
|
|
1316
|
+
"storyVariants": [
|
|
1317
|
+
{
|
|
1318
|
+
"name": "Default",
|
|
1319
|
+
"description": "Ready state.",
|
|
1320
|
+
"args": {
|
|
1321
|
+
"isLoading": false,
|
|
1322
|
+
"error": null
|
|
1323
|
+
},
|
|
1324
|
+
"needsPlayFunction": false
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
"name": "Loading",
|
|
1328
|
+
"description": "Loading state.",
|
|
1329
|
+
"args": {
|
|
1330
|
+
"isLoading": true,
|
|
1331
|
+
"error": null
|
|
1332
|
+
},
|
|
1333
|
+
"needsPlayFunction": false
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
"name": "Error",
|
|
1337
|
+
"description": "Error state.",
|
|
1338
|
+
"args": {
|
|
1339
|
+
"isLoading": false,
|
|
1340
|
+
"error": "Failed to load data"
|
|
1341
|
+
},
|
|
1342
|
+
"needsPlayFunction": false
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
"name": "InteractiveFilter",
|
|
1346
|
+
"description": "Changing filters.",
|
|
1347
|
+
"args": {
|
|
1348
|
+
"isLoading": false,
|
|
1349
|
+
"error": null
|
|
1350
|
+
},
|
|
1351
|
+
"needsPlayFunction": true,
|
|
1352
|
+
"playDescription": "1. userEvent.click(getByRole('combobox', { name: 'Exercise' })), 2. userEvent.click(getByRole('option', { name: 'Squat' })), 3. await waitFor(() => expect(getByText('Loading...')).toBeVisible())"
|
|
1353
|
+
}
|
|
1354
|
+
],
|
|
1355
|
+
"dataContract": {
|
|
1356
|
+
"source": "graphql-query",
|
|
1357
|
+
"operationName": "GetStrengthProgress",
|
|
1358
|
+
"fields": ["date", "value", "exercise"]
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
"componentId": "compete-on-leaderboards-page",
|
|
1363
|
+
"componentName": "CompeteOnLeaderboardsPage",
|
|
1364
|
+
"isNew": true,
|
|
1365
|
+
"atomicType": "page",
|
|
1366
|
+
"composes": ["leaderboard"],
|
|
1367
|
+
"specDeltas": {
|
|
1368
|
+
"structure": [
|
|
1369
|
+
"Page-level component with dedicated route '/compete-on-leaderboards' accepting parameters exerciseId and timePeriod",
|
|
1370
|
+
"Composes Leaderboard in the main content area",
|
|
1371
|
+
"Uses <main> for the leaderboard container with role='main'"
|
|
1372
|
+
],
|
|
1373
|
+
"rendering": [
|
|
1374
|
+
"In loading state, shows Leaderboard with isLoading=true, rendering skeleton rows for table",
|
|
1375
|
+
"In ready state, shows Leaderboard with fetched leaderboardData and userRank",
|
|
1376
|
+
"In error state, shows Leaderboard with error='Failed to load leaderboard data', rendering error banner with retry"
|
|
1377
|
+
],
|
|
1378
|
+
"interaction": [
|
|
1379
|
+
"On page load, fetches leaderboard data via query using route parameters, transitions to loading, on success to ready, on failure to error",
|
|
1380
|
+
"On filter change, updates route parameters and refetches",
|
|
1381
|
+
"On view progress, navigates to view-strength-progress",
|
|
1382
|
+
"On retry, refetches data"
|
|
1383
|
+
],
|
|
1384
|
+
"styling": [
|
|
1385
|
+
"Root page uses `min-h-screen bg-background p-4`.",
|
|
1386
|
+
"Main container uses `max-w-7xl mx-auto` with `useSpring({ opacity: isLoading ? 0 : 1, config: { tension: 400, friction: 26 } })` for fade; respects `useReducedMotion()`.",
|
|
1387
|
+
"In loading state, table skeletons use `h-10 w-full bg-muted animate-pulse` for multiple rows.",
|
|
1388
|
+
"In error state, alert uses `p-4 border border-destructive bg-destructive/10 text-destructive rounded-md flex flex-col gap-2 items-center`, retry button `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md` with hover.",
|
|
1389
|
+
"Page title uses `text-3xl font-bold tracking-tight text-foreground text-balance mb-6`.",
|
|
1390
|
+
"State transitions use `useTransition(state, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: { tension: 300, friction: 22 } })`; respects reduced motion.",
|
|
1391
|
+
"Interactive elements have `min-h-11 min-w-11 touch-action: manipulation`.",
|
|
1392
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `transition: ring-color 150ms ease`.",
|
|
1393
|
+
"Rank numbers use `font-variant-numeric: tabular-nums`.",
|
|
1394
|
+
"Table headings use `text-balance`."
|
|
1395
|
+
]
|
|
1396
|
+
},
|
|
1397
|
+
"props": [
|
|
1398
|
+
{
|
|
1399
|
+
"name": "isLoading",
|
|
1400
|
+
"type": "boolean",
|
|
1401
|
+
"required": false,
|
|
1402
|
+
"default": "false",
|
|
1403
|
+
"description": "Controls loading state.",
|
|
1404
|
+
"category": "state"
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
"name": "error",
|
|
1408
|
+
"type": "string | null",
|
|
1409
|
+
"required": false,
|
|
1410
|
+
"default": "null",
|
|
1411
|
+
"description": "Controls error state.",
|
|
1412
|
+
"category": "state"
|
|
1413
|
+
}
|
|
1414
|
+
],
|
|
1415
|
+
"storyVariants": [
|
|
1416
|
+
{
|
|
1417
|
+
"name": "Default",
|
|
1418
|
+
"description": "Ready state.",
|
|
1419
|
+
"args": {
|
|
1420
|
+
"isLoading": false,
|
|
1421
|
+
"error": null
|
|
1422
|
+
},
|
|
1423
|
+
"needsPlayFunction": false
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
"name": "Loading",
|
|
1427
|
+
"description": "Loading state.",
|
|
1428
|
+
"args": {
|
|
1429
|
+
"isLoading": true,
|
|
1430
|
+
"error": null
|
|
1431
|
+
},
|
|
1432
|
+
"needsPlayFunction": false
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
"name": "Error",
|
|
1436
|
+
"description": "Error state.",
|
|
1437
|
+
"args": {
|
|
1438
|
+
"isLoading": false,
|
|
1439
|
+
"error": "Failed to load data"
|
|
1440
|
+
},
|
|
1441
|
+
"needsPlayFunction": false
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
"name": "InteractiveFilter",
|
|
1445
|
+
"description": "Changing filters.",
|
|
1446
|
+
"args": {
|
|
1447
|
+
"isLoading": false,
|
|
1448
|
+
"error": null
|
|
1449
|
+
},
|
|
1450
|
+
"needsPlayFunction": true,
|
|
1451
|
+
"playDescription": "1. userEvent.click(getByRole('combobox', { name: 'Exercise' })), 2. userEvent.click(getByRole('option', { name: 'Squat' })), 3. await waitFor(() => expect(getByText('Loading...')).toBeVisible())"
|
|
1452
|
+
}
|
|
1453
|
+
],
|
|
1454
|
+
"dataContract": {
|
|
1455
|
+
"source": "graphql-query",
|
|
1456
|
+
"operationName": "GetLeaderboard",
|
|
1457
|
+
"fields": ["rank", "athlete", "performance", "exerciseId", "timePeriod"]
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
]
|