@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,2292 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"componentId": "photo-upload",
|
|
4
|
+
"componentName": "PhotoUpload",
|
|
5
|
+
"isNew": true,
|
|
6
|
+
"atomicType": "molecule",
|
|
7
|
+
"composes": ["ui-components-input", "ui-components-button", "ui-components-aspectratio"],
|
|
8
|
+
"specDeltas": {
|
|
9
|
+
"structure": [
|
|
10
|
+
"Composes Input (type='file' for image upload) and Button for upload trigger",
|
|
11
|
+
"Uses AspectRatio to display uploaded photo preview in a constrained ratio (e.g., 1:1 for square posts)",
|
|
12
|
+
"Wrapper div with semantic section role='group' aria-label='Photo upload'",
|
|
13
|
+
"Button as trigger with text 'Upload Photo' and icon (upload cloud)",
|
|
14
|
+
"After upload, replaces button with image preview using img element inside AspectRatio",
|
|
15
|
+
"Hidden file input associated with button via htmlFor/id pairing"
|
|
16
|
+
],
|
|
17
|
+
"rendering": [
|
|
18
|
+
"In initial state (no photo), renders upload Button",
|
|
19
|
+
"When photo is uploaded (previewUrl prop is set), renders img with src=previewUrl inside AspectRatio, alt='Uploaded post photo'",
|
|
20
|
+
"If isUploading=true, renders Spinner overlay on the preview area",
|
|
21
|
+
"If error prop is truthy, renders Alert below the preview with error message",
|
|
22
|
+
"Uses role='img' on preview container with aria-label='Post photo preview'"
|
|
23
|
+
],
|
|
24
|
+
"interaction": [
|
|
25
|
+
"Clicking upload Button triggers file selection dialog via hidden Input click",
|
|
26
|
+
"On file selection, calls onUpload: (file: File) => void with the selected file",
|
|
27
|
+
"Supports drag-and-drop: on drop, calls onUpload with dropped file if it's an image",
|
|
28
|
+
"Calls onChange: (previewUrl: string) => void when preview is generated",
|
|
29
|
+
"On error during upload, sets internal error state and shows Alert"
|
|
30
|
+
],
|
|
31
|
+
"styling": [
|
|
32
|
+
"Upload button uses `variant=\"default\"` which renders as `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md font-medium`, with additional `w-full flex items-center justify-center gap-2` and icon `h-4 w-4`",
|
|
33
|
+
"Preview container uses `relative` with AspectRatio `aspect-square` (ratio=1), img uses `object-cover w-full h-full rounded-md`",
|
|
34
|
+
"Uploading overlay uses `absolute inset-0 flex items-center justify-center bg-background/80` with Spinner `animate-spin h-8 w-8 text-primary`",
|
|
35
|
+
"Error alert uses `variant=\"destructive\"` which renders as `border-destructive/50 text-destructive dark:border-destructive p-4 rounded-md mt-2`, with AlertTitle `font-semibold` and AlertDescription `text-sm`",
|
|
36
|
+
"Button hover effect wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }`, with CSS `transition: background-color 150ms ease`",
|
|
37
|
+
"Button active state uses CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`",
|
|
38
|
+
"Preview enter animation uses `useTransition(previewUrl, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, config: { tension: 300, friction: 22 } })`, respects `useReducedMotion()` with `immediate: true` when reduced",
|
|
39
|
+
"Touch targets for button use `min-h-11 min-w-11 touch-action: manipulation`",
|
|
40
|
+
"Focus visible on button uses `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`",
|
|
41
|
+
"Skeleton for preview uses `h-64 w-full bg-muted animate-pulse rounded-md` to match content dimensions",
|
|
42
|
+
"Dark mode adjusts overlay to `bg-background/80` with Spinner `text-primary`"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"props": [
|
|
46
|
+
{
|
|
47
|
+
"name": "previewUrl",
|
|
48
|
+
"type": "string | null",
|
|
49
|
+
"required": false,
|
|
50
|
+
"default": "null",
|
|
51
|
+
"description": "URL of the uploaded photo preview.",
|
|
52
|
+
"category": "data"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "isUploading",
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"required": false,
|
|
58
|
+
"default": "false",
|
|
59
|
+
"description": "Whether the upload is in progress.",
|
|
60
|
+
"category": "state"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "error",
|
|
64
|
+
"type": "string | null",
|
|
65
|
+
"required": false,
|
|
66
|
+
"default": "null",
|
|
67
|
+
"description": "Error message to display.",
|
|
68
|
+
"category": "state"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "onUpload",
|
|
72
|
+
"type": "(file: File) => void",
|
|
73
|
+
"required": true,
|
|
74
|
+
"description": "Callback when a file is selected or dropped.",
|
|
75
|
+
"category": "callback"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "onChange",
|
|
79
|
+
"type": "(previewUrl: string) => void",
|
|
80
|
+
"required": false,
|
|
81
|
+
"description": "Callback when preview URL changes.",
|
|
82
|
+
"category": "callback"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "accept",
|
|
86
|
+
"type": "string",
|
|
87
|
+
"required": false,
|
|
88
|
+
"default": "'image/*'",
|
|
89
|
+
"description": "Accepted file types for input.",
|
|
90
|
+
"category": "config"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"storyVariants": [
|
|
94
|
+
{
|
|
95
|
+
"name": "Default",
|
|
96
|
+
"description": "Initial state with upload button.",
|
|
97
|
+
"args": {
|
|
98
|
+
"previewUrl": null,
|
|
99
|
+
"isUploading": false,
|
|
100
|
+
"error": null,
|
|
101
|
+
"onUpload": "fn()",
|
|
102
|
+
"onChange": "fn()"
|
|
103
|
+
},
|
|
104
|
+
"needsPlayFunction": false
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "WithPreview",
|
|
108
|
+
"description": "State with uploaded photo preview.",
|
|
109
|
+
"args": {
|
|
110
|
+
"previewUrl": "https://example.com/photo.jpg",
|
|
111
|
+
"isUploading": false,
|
|
112
|
+
"error": null,
|
|
113
|
+
"onUpload": "fn()",
|
|
114
|
+
"onChange": "fn()"
|
|
115
|
+
},
|
|
116
|
+
"needsPlayFunction": false
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "Uploading",
|
|
120
|
+
"description": "Uploading state with spinner.",
|
|
121
|
+
"args": {
|
|
122
|
+
"previewUrl": null,
|
|
123
|
+
"isUploading": true,
|
|
124
|
+
"error": null,
|
|
125
|
+
"onUpload": "fn()",
|
|
126
|
+
"onChange": "fn()"
|
|
127
|
+
},
|
|
128
|
+
"needsPlayFunction": false
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "Error",
|
|
132
|
+
"description": "Error state with alert.",
|
|
133
|
+
"args": {
|
|
134
|
+
"previewUrl": null,
|
|
135
|
+
"isUploading": false,
|
|
136
|
+
"error": "Upload failed. Please try again.",
|
|
137
|
+
"onUpload": "fn()",
|
|
138
|
+
"onChange": "fn()"
|
|
139
|
+
},
|
|
140
|
+
"needsPlayFunction": false
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "Interactive",
|
|
144
|
+
"description": "For testing file upload interaction.",
|
|
145
|
+
"args": {
|
|
146
|
+
"previewUrl": null,
|
|
147
|
+
"isUploading": false,
|
|
148
|
+
"error": null,
|
|
149
|
+
"onUpload": "fn()",
|
|
150
|
+
"onChange": "fn()"
|
|
151
|
+
},
|
|
152
|
+
"needsPlayFunction": true,
|
|
153
|
+
"playDescription": "1. Click the upload button using userEvent.click(getByRole('button', { name: 'Upload Photo' })), 2. Simulate file selection (cannot fully simulate in Storybook, but assert onUpload called), 3. Wait for preview to appear using await waitFor(() => expect(getByRole('img', { name: 'Post photo preview' })).toBeVisible())"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"dataContract": {
|
|
157
|
+
"source": "props",
|
|
158
|
+
"propsFieldName": "previewUrl",
|
|
159
|
+
"fields": ["previewUrl"]
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"componentId": "caption-input",
|
|
164
|
+
"componentName": "CaptionInput",
|
|
165
|
+
"isNew": true,
|
|
166
|
+
"atomicType": "molecule",
|
|
167
|
+
"composes": ["ui-components-textarea", "ui-components-label"],
|
|
168
|
+
"specDeltas": {
|
|
169
|
+
"structure": [
|
|
170
|
+
"Composes Textarea for caption input and Label for 'Caption' label",
|
|
171
|
+
"Wrapper section with role='group' aria-label='Post caption input'",
|
|
172
|
+
"Label associated with Textarea via htmlFor/id",
|
|
173
|
+
"Textarea with placeholder 'Write a caption...'"
|
|
174
|
+
],
|
|
175
|
+
"rendering": [
|
|
176
|
+
"In valid state, renders Textarea without error",
|
|
177
|
+
"In invalid state (error prop truthy), renders error message below Textarea with aria-describedby linking to error id",
|
|
178
|
+
"Sets aria-invalid='true' on Textarea when error is present",
|
|
179
|
+
"Renders character count below Textarea showing length / maxLength"
|
|
180
|
+
],
|
|
181
|
+
"interaction": [
|
|
182
|
+
"On Textarea change, calls onChange: (value: string) => void",
|
|
183
|
+
"Validates on blur: if value is empty and required, sets internal error 'Caption is required'",
|
|
184
|
+
"Clears error when user starts typing if error was present"
|
|
185
|
+
],
|
|
186
|
+
"styling": [
|
|
187
|
+
"Wrapper uses `flex flex-col gap-1`",
|
|
188
|
+
"Label uses `text-sm font-medium leading-none peer-disabled:opacity-70`",
|
|
189
|
+
"Textarea uses `min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`",
|
|
190
|
+
"Error message uses `text-sm text-destructive mt-1`",
|
|
191
|
+
"Character count uses `text-sm text-muted-foreground self-end mt-1` with `font-variant-numeric: tabular-nums`",
|
|
192
|
+
"Focus visible on textarea uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`",
|
|
193
|
+
"Hover effect on textarea wrapped in `@media (hover: hover) and (pointer: fine) { hover:border-input/80 }` with CSS `transition: border-color 150ms ease`",
|
|
194
|
+
"Invalid state on textarea adds `border-destructive focus-visible:ring-destructive`",
|
|
195
|
+
"Touch targets for textarea use `touch-action: manipulation`, with `text-base` to prevent iOS zoom",
|
|
196
|
+
"Skeleton for textarea uses `h-20 w-full bg-muted animate-pulse rounded-md`",
|
|
197
|
+
"Dark mode uses `bg-background text-foreground border-input` for textarea"
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
"props": [
|
|
201
|
+
{
|
|
202
|
+
"name": "value",
|
|
203
|
+
"type": "string",
|
|
204
|
+
"required": false,
|
|
205
|
+
"default": "''",
|
|
206
|
+
"description": "Current caption text.",
|
|
207
|
+
"category": "data"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"name": "error",
|
|
211
|
+
"type": "string | null",
|
|
212
|
+
"required": false,
|
|
213
|
+
"default": "null",
|
|
214
|
+
"description": "Error message to display.",
|
|
215
|
+
"category": "state"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"name": "onChange",
|
|
219
|
+
"type": "(value: string) => void",
|
|
220
|
+
"required": true,
|
|
221
|
+
"description": "Callback on caption change.",
|
|
222
|
+
"category": "callback"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "maxLength",
|
|
226
|
+
"type": "number",
|
|
227
|
+
"required": false,
|
|
228
|
+
"default": "280",
|
|
229
|
+
"description": "Maximum caption length.",
|
|
230
|
+
"category": "config"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"name": "required",
|
|
234
|
+
"type": "boolean",
|
|
235
|
+
"required": false,
|
|
236
|
+
"default": "true",
|
|
237
|
+
"description": "Whether caption is required.",
|
|
238
|
+
"category": "config"
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"storyVariants": [
|
|
242
|
+
{
|
|
243
|
+
"name": "Default",
|
|
244
|
+
"description": "Empty caption input.",
|
|
245
|
+
"args": {
|
|
246
|
+
"value": "",
|
|
247
|
+
"error": null,
|
|
248
|
+
"onChange": "fn()"
|
|
249
|
+
},
|
|
250
|
+
"needsPlayFunction": false
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "Filled",
|
|
254
|
+
"description": "With caption text.",
|
|
255
|
+
"args": {
|
|
256
|
+
"value": "This is a sample caption.",
|
|
257
|
+
"error": null,
|
|
258
|
+
"onChange": "fn()"
|
|
259
|
+
},
|
|
260
|
+
"needsPlayFunction": false
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "Error",
|
|
264
|
+
"description": "With validation error.",
|
|
265
|
+
"args": {
|
|
266
|
+
"value": "",
|
|
267
|
+
"error": "Caption is required",
|
|
268
|
+
"onChange": "fn()"
|
|
269
|
+
},
|
|
270
|
+
"needsPlayFunction": false
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "Interactive",
|
|
274
|
+
"description": "For testing input and validation.",
|
|
275
|
+
"args": {
|
|
276
|
+
"value": "",
|
|
277
|
+
"error": null,
|
|
278
|
+
"onChange": "fn()"
|
|
279
|
+
},
|
|
280
|
+
"needsPlayFunction": true,
|
|
281
|
+
"playDescription": "1. Type 'Test' into textarea using userEvent.type(getByLabelText('Caption'), 'Test'), 2. Blur the textarea using userEvent.tab(), 3. Assert no error visible since not empty, 4. Clear text using userEvent.clear(getByLabelText('Caption')), 5. Blur again, 6. Await waitFor(() => expect(getByText('Caption is required')).toBeVisible())"
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
"dataContract": {
|
|
285
|
+
"source": "props",
|
|
286
|
+
"propsFieldName": "value",
|
|
287
|
+
"fields": ["value"]
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"componentId": "post-preview",
|
|
292
|
+
"componentName": "PostPreview",
|
|
293
|
+
"isNew": true,
|
|
294
|
+
"atomicType": "molecule",
|
|
295
|
+
"composes": ["ui-components-card", "ui-components-aspectratio"],
|
|
296
|
+
"specDeltas": {
|
|
297
|
+
"structure": [
|
|
298
|
+
"Composes Card to wrap preview content",
|
|
299
|
+
"Uses AspectRatio for photo display",
|
|
300
|
+
"Includes CardContent with caption text",
|
|
301
|
+
"Semantic article role with aria-label='Post preview'"
|
|
302
|
+
],
|
|
303
|
+
"rendering": [
|
|
304
|
+
"If photoUrl prop is set, renders img with src=photoUrl inside AspectRatio, alt='Post photo'",
|
|
305
|
+
"Renders caption prop as paragraph text below photo",
|
|
306
|
+
"If no photoUrl, renders placeholder Skeleton in AspectRatio",
|
|
307
|
+
"If no caption, renders empty paragraph or nothing"
|
|
308
|
+
],
|
|
309
|
+
"interaction": ["No interactive callbacks, display-only"],
|
|
310
|
+
"styling": [
|
|
311
|
+
"Card wrapper uses `rounded-lg border bg-card text-card-foreground shadow-sm`",
|
|
312
|
+
"AspectRatio for photo uses `aspect-square` with img `object-cover w-full h-full rounded-t-lg`",
|
|
313
|
+
"Caption paragraph uses `p-4 text-sm text-foreground` with `text-wrap: balance`",
|
|
314
|
+
"Skeleton for photo uses `h-64 w-full bg-muted animate-pulse rounded-t-lg` to match content",
|
|
315
|
+
"Skeleton for caption uses `h-4 w-3/4 bg-muted animate-pulse mx-4 mb-4`",
|
|
316
|
+
"Enter animation for content uses `useTransition(true, { from: { opacity: 0 }, enter: { opacity: 1 }, config: { tension: 400, friction: 26 } })`, respects `useReducedMotion()` with `immediate: true`",
|
|
317
|
+
"No interactive elements, but decorative img has `pointer-events: none`",
|
|
318
|
+
"Dark mode uses `bg-card text-card-foreground border-border`",
|
|
319
|
+
"Subtle separator between photo and caption uses `box-shadow: 0 0 0 1px rgba(0,0,0,0.08)` instead of border",
|
|
320
|
+
"Heading (if any) uses `text-balance` for short text"
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
"props": [
|
|
324
|
+
{
|
|
325
|
+
"name": "photoUrl",
|
|
326
|
+
"type": "string | null",
|
|
327
|
+
"required": false,
|
|
328
|
+
"default": "null",
|
|
329
|
+
"description": "URL of the post photo.",
|
|
330
|
+
"category": "data"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"name": "caption",
|
|
334
|
+
"type": "string",
|
|
335
|
+
"required": false,
|
|
336
|
+
"default": "''",
|
|
337
|
+
"description": "Post caption text.",
|
|
338
|
+
"category": "data"
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
"storyVariants": [
|
|
342
|
+
{
|
|
343
|
+
"name": "Default",
|
|
344
|
+
"description": "Empty preview with skeletons.",
|
|
345
|
+
"args": {
|
|
346
|
+
"photoUrl": null,
|
|
347
|
+
"caption": ""
|
|
348
|
+
},
|
|
349
|
+
"needsPlayFunction": false
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"name": "WithContent",
|
|
353
|
+
"description": "Preview with photo and caption.",
|
|
354
|
+
"args": {
|
|
355
|
+
"photoUrl": "https://example.com/photo.jpg",
|
|
356
|
+
"caption": "Sample caption"
|
|
357
|
+
},
|
|
358
|
+
"needsPlayFunction": false
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
"dataContract": {
|
|
362
|
+
"source": "props",
|
|
363
|
+
"propsFieldName": "photoUrl",
|
|
364
|
+
"fields": ["photoUrl", "caption"]
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"componentId": "like-button",
|
|
369
|
+
"componentName": "LikeButton",
|
|
370
|
+
"isNew": true,
|
|
371
|
+
"atomicType": "molecule",
|
|
372
|
+
"composes": ["ui-components-toggle"],
|
|
373
|
+
"specDeltas": {
|
|
374
|
+
"structure": [
|
|
375
|
+
"Composes Toggle with heart icon",
|
|
376
|
+
"Includes span for like count",
|
|
377
|
+
"Wrapper button with role='button' aria-label='Like post'"
|
|
378
|
+
],
|
|
379
|
+
"rendering": [
|
|
380
|
+
"When liked (pressed=true), renders filled heart icon",
|
|
381
|
+
"When not liked (pressed=false), renders outline heart icon",
|
|
382
|
+
"Renders likeCount prop as text next to icon",
|
|
383
|
+
"If isLoading=true, disables toggle and shows Spinner"
|
|
384
|
+
],
|
|
385
|
+
"interaction": [
|
|
386
|
+
"Clicking toggles pressed state and calls onToggle: (liked: boolean) => void",
|
|
387
|
+
"Optimistically updates likeCount before callback resolves",
|
|
388
|
+
"On error, rolls back likeCount and shows toast error"
|
|
389
|
+
],
|
|
390
|
+
"styling": [
|
|
391
|
+
"Toggle button uses `variant=\"ghost\"` which renders as `bg-transparent hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2`, with `flex items-center gap-1`",
|
|
392
|
+
"Heart icon uses `h-5 w-5` with conditional `fill-current text-red-500` when pressed, else `text-muted-foreground`",
|
|
393
|
+
"Like count uses `text-sm font-medium` with `font-variant-numeric: tabular-nums`",
|
|
394
|
+
"Loading spinner uses `animate-spin h-5 w-5 text-muted-foreground` replacing icon when isLoading",
|
|
395
|
+
"Hover effect wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }` with CSS `transition: background-color 150ms ease`",
|
|
396
|
+
"Active state uses CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`",
|
|
397
|
+
"Focus visible uses `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`",
|
|
398
|
+
"Disabled state adds `opacity-50 cursor-not-allowed`",
|
|
399
|
+
"Toggle animation uses `useSpring({ scale: pressed ? 1.1 : 1, config: { tension: 400, friction: 22 } })` for icon, respects `useReducedMotion()` with `immediate: true`",
|
|
400
|
+
"Touch targets use `min-h-11 min-w-11 touch-action: manipulation`",
|
|
401
|
+
"Dark mode uses `hover:bg-accent hover:text-accent-foreground`"
|
|
402
|
+
]
|
|
403
|
+
},
|
|
404
|
+
"props": [
|
|
405
|
+
{
|
|
406
|
+
"name": "pressed",
|
|
407
|
+
"type": "boolean",
|
|
408
|
+
"required": false,
|
|
409
|
+
"default": "false",
|
|
410
|
+
"description": "Whether the post is liked.",
|
|
411
|
+
"category": "state"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"name": "likeCount",
|
|
415
|
+
"type": "number",
|
|
416
|
+
"required": false,
|
|
417
|
+
"default": "0",
|
|
418
|
+
"description": "Number of likes.",
|
|
419
|
+
"category": "data"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"name": "isLoading",
|
|
423
|
+
"type": "boolean",
|
|
424
|
+
"required": false,
|
|
425
|
+
"default": "false",
|
|
426
|
+
"description": "Whether like action is in progress.",
|
|
427
|
+
"category": "state"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"name": "onToggle",
|
|
431
|
+
"type": "(liked: boolean) => void",
|
|
432
|
+
"required": true,
|
|
433
|
+
"description": "Callback on like toggle.",
|
|
434
|
+
"category": "callback"
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
"storyVariants": [
|
|
438
|
+
{
|
|
439
|
+
"name": "Default",
|
|
440
|
+
"description": "Unliked state.",
|
|
441
|
+
"args": {
|
|
442
|
+
"pressed": false,
|
|
443
|
+
"likeCount": 0,
|
|
444
|
+
"isLoading": false,
|
|
445
|
+
"onToggle": "fn()"
|
|
446
|
+
},
|
|
447
|
+
"needsPlayFunction": false
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
"name": "Liked",
|
|
451
|
+
"description": "Liked state with count.",
|
|
452
|
+
"args": {
|
|
453
|
+
"pressed": true,
|
|
454
|
+
"likeCount": 5,
|
|
455
|
+
"isLoading": false,
|
|
456
|
+
"onToggle": "fn()"
|
|
457
|
+
},
|
|
458
|
+
"needsPlayFunction": false
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
"name": "Loading",
|
|
462
|
+
"description": "Loading state.",
|
|
463
|
+
"args": {
|
|
464
|
+
"pressed": false,
|
|
465
|
+
"likeCount": 0,
|
|
466
|
+
"isLoading": true,
|
|
467
|
+
"onToggle": "fn()"
|
|
468
|
+
},
|
|
469
|
+
"needsPlayFunction": false
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"name": "Interactive",
|
|
473
|
+
"description": "For testing toggle.",
|
|
474
|
+
"args": {
|
|
475
|
+
"pressed": false,
|
|
476
|
+
"likeCount": 0,
|
|
477
|
+
"isLoading": false,
|
|
478
|
+
"onToggle": "fn()"
|
|
479
|
+
},
|
|
480
|
+
"needsPlayFunction": true,
|
|
481
|
+
"playDescription": "1. Click the button using userEvent.click(getByRole('button', { name: 'Like post' })), 2. Await waitFor(() => expect(getByRole('button')).toHaveAttribute('aria-pressed', 'true')), 3. Assert like count increased to 1"
|
|
482
|
+
}
|
|
483
|
+
],
|
|
484
|
+
"dataContract": {
|
|
485
|
+
"source": "props",
|
|
486
|
+
"propsFieldName": "likeCount",
|
|
487
|
+
"fields": ["likeCount"]
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
"componentId": "comment-input",
|
|
492
|
+
"componentName": "CommentInput",
|
|
493
|
+
"isNew": true,
|
|
494
|
+
"atomicType": "molecule",
|
|
495
|
+
"composes": ["ui-components-textarea", "ui-components-button"],
|
|
496
|
+
"specDeltas": {
|
|
497
|
+
"structure": [
|
|
498
|
+
"Composes Textarea for comment text and Button for submit",
|
|
499
|
+
"Wrapper div with role='group' aria-label='Add comment'",
|
|
500
|
+
"Button with text 'Post' positioned after Textarea"
|
|
501
|
+
],
|
|
502
|
+
"rendering": [
|
|
503
|
+
"Renders Textarea with placeholder 'Add a comment...'",
|
|
504
|
+
"If error prop truthy, renders error text below Textarea with aria-describedby",
|
|
505
|
+
"Disables Button when value is empty or isSubmitting=true",
|
|
506
|
+
"Sets aria-invalid on Textarea when error present"
|
|
507
|
+
],
|
|
508
|
+
"interaction": [
|
|
509
|
+
"On Textarea change, calls onChange: (value: string) => void",
|
|
510
|
+
"Clicking Button calls onSubmit: (comment: string) => void if value not empty",
|
|
511
|
+
"On submit, sets isSubmitting=true, clears value on success",
|
|
512
|
+
"Validates on submit: if empty, shows 'Comment cannot be empty' error"
|
|
513
|
+
],
|
|
514
|
+
"styling": [
|
|
515
|
+
"Wrapper uses `flex items-end gap-2`",
|
|
516
|
+
"Textarea uses `flex-1 min-h-[40px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`",
|
|
517
|
+
"Submit button uses `variant=\"default\" size=\"sm\"` which renders as `bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-3 rounded-md`",
|
|
518
|
+
"Error message uses `text-sm text-destructive mt-1 w-full`",
|
|
519
|
+
"Focus visible on textarea and button uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`",
|
|
520
|
+
"Hover on button wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }` with CSS `transition: background-color 150ms ease`",
|
|
521
|
+
"Disabled button adds `opacity-50 cursor-not-allowed`",
|
|
522
|
+
"Submitting state on button replaces text with Spinner `animate-spin h-4 w-4`",
|
|
523
|
+
"Touch targets for button use `min-h-11 min-w-11 touch-action: manipulation`, textarea `text-base`",
|
|
524
|
+
"Skeleton for textarea uses `h-10 w-full bg-muted animate-pulse rounded-md`, for button `h-9 w-20 bg-muted animate-pulse rounded-md`",
|
|
525
|
+
"Dark mode uses `bg-background text-foreground border-input` for textarea"
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
"props": [
|
|
529
|
+
{
|
|
530
|
+
"name": "value",
|
|
531
|
+
"type": "string",
|
|
532
|
+
"required": false,
|
|
533
|
+
"default": "''",
|
|
534
|
+
"description": "Current comment text.",
|
|
535
|
+
"category": "data"
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"name": "error",
|
|
539
|
+
"type": "string | null",
|
|
540
|
+
"required": false,
|
|
541
|
+
"default": "null",
|
|
542
|
+
"description": "Error message.",
|
|
543
|
+
"category": "state"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "isSubmitting",
|
|
547
|
+
"type": "boolean",
|
|
548
|
+
"required": false,
|
|
549
|
+
"default": "false",
|
|
550
|
+
"description": "Whether comment is submitting.",
|
|
551
|
+
"category": "state"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"name": "onChange",
|
|
555
|
+
"type": "(value: string) => void",
|
|
556
|
+
"required": true,
|
|
557
|
+
"description": "Callback on text change.",
|
|
558
|
+
"category": "callback"
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"name": "onSubmit",
|
|
562
|
+
"type": "(comment: string) => void",
|
|
563
|
+
"required": true,
|
|
564
|
+
"description": "Callback on submit.",
|
|
565
|
+
"category": "callback"
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
"storyVariants": [
|
|
569
|
+
{
|
|
570
|
+
"name": "Default",
|
|
571
|
+
"description": "Empty comment input.",
|
|
572
|
+
"args": {
|
|
573
|
+
"value": "",
|
|
574
|
+
"error": null,
|
|
575
|
+
"isSubmitting": false,
|
|
576
|
+
"onChange": "fn()",
|
|
577
|
+
"onSubmit": "fn()"
|
|
578
|
+
},
|
|
579
|
+
"needsPlayFunction": false
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"name": "Filled",
|
|
583
|
+
"description": "With comment text.",
|
|
584
|
+
"args": {
|
|
585
|
+
"value": "Great post!",
|
|
586
|
+
"error": null,
|
|
587
|
+
"isSubmitting": false,
|
|
588
|
+
"onChange": "fn()",
|
|
589
|
+
"onSubmit": "fn()"
|
|
590
|
+
},
|
|
591
|
+
"needsPlayFunction": false
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
"name": "Submitting",
|
|
595
|
+
"description": "Submitting state.",
|
|
596
|
+
"args": {
|
|
597
|
+
"value": "Great post!",
|
|
598
|
+
"error": null,
|
|
599
|
+
"isSubmitting": true,
|
|
600
|
+
"onChange": "fn()",
|
|
601
|
+
"onSubmit": "fn()"
|
|
602
|
+
},
|
|
603
|
+
"needsPlayFunction": false
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"name": "Error",
|
|
607
|
+
"description": "With error.",
|
|
608
|
+
"args": {
|
|
609
|
+
"value": "",
|
|
610
|
+
"error": "Comment cannot be empty",
|
|
611
|
+
"isSubmitting": false,
|
|
612
|
+
"onChange": "fn()",
|
|
613
|
+
"onSubmit": "fn()"
|
|
614
|
+
},
|
|
615
|
+
"needsPlayFunction": false
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"name": "Interactive",
|
|
619
|
+
"description": "For testing submit.",
|
|
620
|
+
"args": {
|
|
621
|
+
"value": "",
|
|
622
|
+
"error": null,
|
|
623
|
+
"isSubmitting": false,
|
|
624
|
+
"onChange": "fn()",
|
|
625
|
+
"onSubmit": "fn()"
|
|
626
|
+
},
|
|
627
|
+
"needsPlayFunction": true,
|
|
628
|
+
"playDescription": "1. Type 'Test comment' into textarea using userEvent.type(getByPlaceholderText('Add a comment...'), 'Test comment'), 2. Click submit button using userEvent.click(getByRole('button', { name: 'Post' })), 3. Await waitFor(() => expect(getByRole('button')).toBeDisabled()), 4. Assert onSubmit called with 'Test comment'"
|
|
629
|
+
}
|
|
630
|
+
],
|
|
631
|
+
"dataContract": {
|
|
632
|
+
"source": "props",
|
|
633
|
+
"propsFieldName": "value",
|
|
634
|
+
"fields": ["value"]
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"componentId": "comments-list",
|
|
639
|
+
"componentName": "CommentsList",
|
|
640
|
+
"isNew": true,
|
|
641
|
+
"atomicType": "organism",
|
|
642
|
+
"composes": ["ui-components-avatar", "ui-components-separator"],
|
|
643
|
+
"specDeltas": {
|
|
644
|
+
"structure": [
|
|
645
|
+
"Uses ul with role='list' for comments",
|
|
646
|
+
"Each comment as li composing Avatar for user, p for text, time for timestamp",
|
|
647
|
+
"Separator between comments"
|
|
648
|
+
],
|
|
649
|
+
"rendering": [
|
|
650
|
+
"Renders comments prop as list items",
|
|
651
|
+
"Each item shows user avatar, name, comment text, timestamp",
|
|
652
|
+
"If isLoading=true, renders Skeleton for 3 placeholder comments",
|
|
653
|
+
"If error prop truthy, renders Alert with error message",
|
|
654
|
+
"If comments.length === 0, renders empty message 'No comments yet'"
|
|
655
|
+
],
|
|
656
|
+
"interaction": [
|
|
657
|
+
"No direct interactions, but supports onReply: (commentId: string) => void for reply buttons if provided"
|
|
658
|
+
],
|
|
659
|
+
"styling": [
|
|
660
|
+
"List wrapper uses `flex flex-col gap-4` with role='list'",
|
|
661
|
+
"Each comment li uses `flex gap-3` with Separator `h-px bg-border my-4` between items",
|
|
662
|
+
"Avatar uses `h-8 w-8 rounded-full`",
|
|
663
|
+
"User name uses `text-sm font-semibold`",
|
|
664
|
+
"Comment text uses `text-sm text-foreground` with `text-wrap: balance`",
|
|
665
|
+
"Timestamp uses `text-xs text-muted-foreground` with `font-variant-numeric: tabular-nums`",
|
|
666
|
+
"Skeleton comment uses `flex gap-3` with avatar `h-8 w-8 bg-muted animate-pulse rounded-full`, text lines `h-4 w-32 bg-muted animate-pulse`, `h-4 w-48 bg-muted animate-pulse`",
|
|
667
|
+
"Empty message uses `text-center text-sm text-muted-foreground py-4`",
|
|
668
|
+
"Error alert uses `variant=\"destructive\" p-4 rounded-md`",
|
|
669
|
+
"List enter animation uses `useTrail(comments.length, { opacity: 1, transform: 'translateY(0)', from: { opacity: 0, transform: 'translateY(8px)' }, config: { tension: 300, friction: 22 } })` for staggered items, respects `useReducedMotion()`",
|
|
670
|
+
"Dark mode uses `bg-card text-card-foreground` for comments"
|
|
671
|
+
]
|
|
672
|
+
},
|
|
673
|
+
"props": [
|
|
674
|
+
{
|
|
675
|
+
"name": "comments",
|
|
676
|
+
"type": "Array<{ id: string; user: { name: string; avatar: string }; text: string; timestamp: string }>",
|
|
677
|
+
"required": true,
|
|
678
|
+
"description": "List of comments.",
|
|
679
|
+
"category": "data"
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
"name": "isLoading",
|
|
683
|
+
"type": "boolean",
|
|
684
|
+
"required": false,
|
|
685
|
+
"default": "false",
|
|
686
|
+
"description": "Whether comments are loading.",
|
|
687
|
+
"category": "state"
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"name": "error",
|
|
691
|
+
"type": "string | null",
|
|
692
|
+
"required": false,
|
|
693
|
+
"default": "null",
|
|
694
|
+
"description": "Error message.",
|
|
695
|
+
"category": "state"
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"name": "onReply",
|
|
699
|
+
"type": "(commentId: string) => void",
|
|
700
|
+
"required": false,
|
|
701
|
+
"description": "Callback for replying to a comment.",
|
|
702
|
+
"category": "callback"
|
|
703
|
+
}
|
|
704
|
+
],
|
|
705
|
+
"storyVariants": [
|
|
706
|
+
{
|
|
707
|
+
"name": "Default",
|
|
708
|
+
"description": "List with comments.",
|
|
709
|
+
"args": {
|
|
710
|
+
"comments": [
|
|
711
|
+
{
|
|
712
|
+
"id": "1",
|
|
713
|
+
"user": {
|
|
714
|
+
"name": "User1",
|
|
715
|
+
"avatar": "https://example.com/avatar1.jpg"
|
|
716
|
+
},
|
|
717
|
+
"text": "Nice post!",
|
|
718
|
+
"timestamp": "2024-03-15T12:00:00Z"
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
"id": "2",
|
|
722
|
+
"user": {
|
|
723
|
+
"name": "User2",
|
|
724
|
+
"avatar": "https://example.com/avatar2.jpg"
|
|
725
|
+
},
|
|
726
|
+
"text": "Agreed!",
|
|
727
|
+
"timestamp": "2024-03-15T12:05:00Z"
|
|
728
|
+
}
|
|
729
|
+
],
|
|
730
|
+
"isLoading": false,
|
|
731
|
+
"error": null,
|
|
732
|
+
"onReply": "fn()"
|
|
733
|
+
},
|
|
734
|
+
"needsPlayFunction": false
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
"name": "Loading",
|
|
738
|
+
"description": "Loading state with skeletons.",
|
|
739
|
+
"args": {
|
|
740
|
+
"comments": [],
|
|
741
|
+
"isLoading": true,
|
|
742
|
+
"error": null,
|
|
743
|
+
"onReply": "fn()"
|
|
744
|
+
},
|
|
745
|
+
"needsPlayFunction": false
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"name": "Error",
|
|
749
|
+
"description": "Error state.",
|
|
750
|
+
"args": {
|
|
751
|
+
"comments": [],
|
|
752
|
+
"isLoading": false,
|
|
753
|
+
"error": "Failed to load comments",
|
|
754
|
+
"onReply": "fn()"
|
|
755
|
+
},
|
|
756
|
+
"needsPlayFunction": false
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
"name": "Empty",
|
|
760
|
+
"description": "Empty list.",
|
|
761
|
+
"args": {
|
|
762
|
+
"comments": [],
|
|
763
|
+
"isLoading": false,
|
|
764
|
+
"error": null,
|
|
765
|
+
"onReply": "fn()"
|
|
766
|
+
},
|
|
767
|
+
"needsPlayFunction": false
|
|
768
|
+
}
|
|
769
|
+
],
|
|
770
|
+
"dataContract": {
|
|
771
|
+
"source": "props",
|
|
772
|
+
"propsFieldName": "comments",
|
|
773
|
+
"fields": ["comments.id", "comments.user.name", "comments.user.avatar", "comments.text", "comments.timestamp"]
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
"componentId": "post-card",
|
|
778
|
+
"componentName": "PostCard",
|
|
779
|
+
"isNew": true,
|
|
780
|
+
"atomicType": "organism",
|
|
781
|
+
"composes": ["ui-components-card", "ui-components-aspectratio", "ui-components-avatar", "ui-components-button"],
|
|
782
|
+
"specDeltas": {
|
|
783
|
+
"structure": [
|
|
784
|
+
"Composes Card with header (user Avatar and name), media (img in AspectRatio), footer (caption, like count, comment count)",
|
|
785
|
+
"Button for view details or navigate"
|
|
786
|
+
],
|
|
787
|
+
"rendering": [
|
|
788
|
+
"Renders user avatar and name in CardHeader",
|
|
789
|
+
"Renders post photo in AspectRatio with alt='Post photo'",
|
|
790
|
+
"Renders caption in CardContent",
|
|
791
|
+
"Renders like and comment icons with counts in CardFooter",
|
|
792
|
+
"If isLoading=true, renders Skeleton for media and text"
|
|
793
|
+
],
|
|
794
|
+
"interaction": ["Clicking the card calls onClick: (postId: string) => void to navigate to detail"],
|
|
795
|
+
"styling": [
|
|
796
|
+
"Card wrapper uses `rounded-lg border bg-card text-card-foreground shadow-sm hover:shadow-md` with CSS `transition: box-shadow 150ms ease`",
|
|
797
|
+
"Header uses `flex items-center gap-2 p-4` with Avatar `h-8 w-8`, name `text-sm font-semibold`",
|
|
798
|
+
"Media uses AspectRatio `aspect-video` with img `object-cover w-full h-full rounded-md`",
|
|
799
|
+
"Caption uses `p-4 text-sm text-foreground` with `text-wrap: balance`",
|
|
800
|
+
"Footer uses `flex justify-between p-4 text-sm text-muted-foreground` with icons `h-4 w-4` and counts `font-variant-numeric: tabular-nums`",
|
|
801
|
+
"Hover effect wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent/10 }` with CSS `transition: background-color 150ms ease`",
|
|
802
|
+
"Focus visible uses `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`",
|
|
803
|
+
"Skeleton for media uses `h-48 w-full bg-muted animate-pulse rounded-md`",
|
|
804
|
+
"Skeleton for caption uses `h-4 w-3/4 bg-muted animate-pulse mx-4`",
|
|
805
|
+
"Enter animation uses `useTransition(true, { from: { opacity: 0, transform: 'translateY(8px)' }, enter: { opacity: 1, transform: 'translateY(0)' }, config: { tension: 300, friction: 22 } })`, respects `useReducedMotion()`",
|
|
806
|
+
"Touch targets for card use `touch-action: manipulation`",
|
|
807
|
+
"Dark mode uses `bg-card text-card-foreground border-border`"
|
|
808
|
+
]
|
|
809
|
+
},
|
|
810
|
+
"props": [
|
|
811
|
+
{
|
|
812
|
+
"name": "post",
|
|
813
|
+
"type": "{ id: string; user: { name: string; avatar: string }; photoUrl: string; caption: string; likeCount: number; commentCount: number }",
|
|
814
|
+
"required": true,
|
|
815
|
+
"description": "Post data.",
|
|
816
|
+
"category": "data"
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
"name": "isLoading",
|
|
820
|
+
"type": "boolean",
|
|
821
|
+
"required": false,
|
|
822
|
+
"default": "false",
|
|
823
|
+
"description": "Whether post is loading.",
|
|
824
|
+
"category": "state"
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
"name": "onClick",
|
|
828
|
+
"type": "(postId: string) => void",
|
|
829
|
+
"required": false,
|
|
830
|
+
"description": "Callback on card click.",
|
|
831
|
+
"category": "callback"
|
|
832
|
+
}
|
|
833
|
+
],
|
|
834
|
+
"storyVariants": [
|
|
835
|
+
{
|
|
836
|
+
"name": "Default",
|
|
837
|
+
"description": "Post card with data.",
|
|
838
|
+
"args": {
|
|
839
|
+
"post": {
|
|
840
|
+
"id": "1",
|
|
841
|
+
"user": {
|
|
842
|
+
"name": "User1",
|
|
843
|
+
"avatar": "https://example.com/avatar.jpg"
|
|
844
|
+
},
|
|
845
|
+
"photoUrl": "https://example.com/photo.jpg",
|
|
846
|
+
"caption": "Sample post",
|
|
847
|
+
"likeCount": 10,
|
|
848
|
+
"commentCount": 2
|
|
849
|
+
},
|
|
850
|
+
"isLoading": false,
|
|
851
|
+
"onClick": "fn()"
|
|
852
|
+
},
|
|
853
|
+
"needsPlayFunction": true,
|
|
854
|
+
"playDescription": "1. Click the card using userEvent.click(getByRole('article')), 2. Assert onClick called with post id '1'"
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
"name": "Loading",
|
|
858
|
+
"description": "Loading state.",
|
|
859
|
+
"args": {
|
|
860
|
+
"post": {
|
|
861
|
+
"id": "",
|
|
862
|
+
"user": {
|
|
863
|
+
"name": "",
|
|
864
|
+
"avatar": ""
|
|
865
|
+
},
|
|
866
|
+
"photoUrl": "",
|
|
867
|
+
"caption": "",
|
|
868
|
+
"likeCount": 0,
|
|
869
|
+
"commentCount": 0
|
|
870
|
+
},
|
|
871
|
+
"isLoading": true,
|
|
872
|
+
"onClick": "fn()"
|
|
873
|
+
},
|
|
874
|
+
"needsPlayFunction": false
|
|
875
|
+
}
|
|
876
|
+
],
|
|
877
|
+
"dataContract": {
|
|
878
|
+
"source": "props",
|
|
879
|
+
"propsFieldName": "post",
|
|
880
|
+
"fields": [
|
|
881
|
+
"post.id",
|
|
882
|
+
"post.user.name",
|
|
883
|
+
"post.user.avatar",
|
|
884
|
+
"post.photoUrl",
|
|
885
|
+
"post.caption",
|
|
886
|
+
"post.likeCount",
|
|
887
|
+
"post.commentCount"
|
|
888
|
+
]
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
"componentId": "posts-list",
|
|
893
|
+
"componentName": "PostsList",
|
|
894
|
+
"isNew": true,
|
|
895
|
+
"atomicType": "organism",
|
|
896
|
+
"composes": ["post-card", "ui-components-scrollarea"],
|
|
897
|
+
"specDeltas": {
|
|
898
|
+
"structure": [
|
|
899
|
+
"Uses ScrollArea for infinite scroll list",
|
|
900
|
+
"Renders array of PostCard for each post",
|
|
901
|
+
"Includes load more trigger at bottom"
|
|
902
|
+
],
|
|
903
|
+
"rendering": [
|
|
904
|
+
"In ready state, renders posts prop as PostCard list",
|
|
905
|
+
"In loading state, renders Skeleton PostCard placeholders (e.g., 5 items)",
|
|
906
|
+
"In error state, renders Alert with retry Button",
|
|
907
|
+
"In empty state, renders empty message with create Button",
|
|
908
|
+
"If hasMore=true, renders load more Button or auto-triggers on scroll"
|
|
909
|
+
],
|
|
910
|
+
"interaction": [
|
|
911
|
+
"On scroll to bottom, calls onLoadMore: () => void if hasMore",
|
|
912
|
+
"Clicking retry in error state calls onRetry: () => void",
|
|
913
|
+
"Pull-to-refresh gesture calls onRefresh: () => void"
|
|
914
|
+
],
|
|
915
|
+
"styling": [
|
|
916
|
+
"ScrollArea uses `h-full w-full` with vertical scrollbar `w-2` thumb `bg-border rounded-full`",
|
|
917
|
+
"List uses `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4`",
|
|
918
|
+
"Load more button uses `variant=\"ghost\" w-full py-4 text-center text-muted-foreground hover:bg-accent`",
|
|
919
|
+
"Error alert uses `variant=\"destructive\" p-4 rounded-md m-4` with retry button `variant=\"default\" mt-2`",
|
|
920
|
+
"Empty message uses `text-center text-muted-foreground py-8` with create button `variant=\"primary\" mt-4`",
|
|
921
|
+
"Skeleton list uses 5 items each `h-64 w-full bg-muted animate-pulse rounded-lg` with `grid` layout",
|
|
922
|
+
"Infinite scroll trigger uses `h-px w-full` at bottom",
|
|
923
|
+
"Pull-to-refresh indicator uses `absolute top-0 left-0 right-0 flex justify-center py-2` with Spinner `animate-spin h-6 w-6` when pulling",
|
|
924
|
+
"Item enter animation uses `useTrail(posts.length, { opacity: 1, transform: 'translateY(0)', from: { opacity: 0, transform: 'translateY(20px)' }, config: { tension: 170, friction: 26 } })` for gentle stagger, respects `useReducedMotion()`",
|
|
925
|
+
"Hover on load more wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }` with CSS `transition: background-color 150ms ease`",
|
|
926
|
+
"Touch targets for buttons use `min-h-11 touch-action: manipulation`",
|
|
927
|
+
"Dark mode uses `bg-background` for list"
|
|
928
|
+
]
|
|
929
|
+
},
|
|
930
|
+
"props": [
|
|
931
|
+
{
|
|
932
|
+
"name": "posts",
|
|
933
|
+
"type": "Array<{ id: string; user: { name: string; avatar: string }; photoUrl: string; caption: string; likeCount: number; commentCount: number }>",
|
|
934
|
+
"required": true,
|
|
935
|
+
"description": "List of posts.",
|
|
936
|
+
"category": "data"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"name": "isLoading",
|
|
940
|
+
"type": "boolean",
|
|
941
|
+
"required": false,
|
|
942
|
+
"default": "false",
|
|
943
|
+
"description": "Loading state.",
|
|
944
|
+
"category": "state"
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
"name": "error",
|
|
948
|
+
"type": "string | null",
|
|
949
|
+
"required": false,
|
|
950
|
+
"default": "null",
|
|
951
|
+
"description": "Error message.",
|
|
952
|
+
"category": "state"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
"name": "hasMore",
|
|
956
|
+
"type": "boolean",
|
|
957
|
+
"required": false,
|
|
958
|
+
"default": "false",
|
|
959
|
+
"description": "Whether more posts available.",
|
|
960
|
+
"category": "state"
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
"name": "onLoadMore",
|
|
964
|
+
"type": "() => void",
|
|
965
|
+
"required": false,
|
|
966
|
+
"description": "Callback to load more.",
|
|
967
|
+
"category": "callback"
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
"name": "onRetry",
|
|
971
|
+
"type": "() => void",
|
|
972
|
+
"required": false,
|
|
973
|
+
"description": "Callback to retry load.",
|
|
974
|
+
"category": "callback"
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
"name": "onRefresh",
|
|
978
|
+
"type": "() => void",
|
|
979
|
+
"required": false,
|
|
980
|
+
"description": "Callback on pull-to-refresh.",
|
|
981
|
+
"category": "callback"
|
|
982
|
+
}
|
|
983
|
+
],
|
|
984
|
+
"storyVariants": [
|
|
985
|
+
{
|
|
986
|
+
"name": "Default",
|
|
987
|
+
"description": "List with posts.",
|
|
988
|
+
"args": {
|
|
989
|
+
"posts": [
|
|
990
|
+
{
|
|
991
|
+
"id": "1",
|
|
992
|
+
"user": {
|
|
993
|
+
"name": "User1",
|
|
994
|
+
"avatar": "avatar1"
|
|
995
|
+
},
|
|
996
|
+
"photoUrl": "photo1",
|
|
997
|
+
"caption": "Cap1",
|
|
998
|
+
"likeCount": 10,
|
|
999
|
+
"commentCount": 2
|
|
1000
|
+
}
|
|
1001
|
+
],
|
|
1002
|
+
"isLoading": false,
|
|
1003
|
+
"error": null,
|
|
1004
|
+
"hasMore": false,
|
|
1005
|
+
"onLoadMore": "fn()",
|
|
1006
|
+
"onRetry": "fn()",
|
|
1007
|
+
"onRefresh": "fn()"
|
|
1008
|
+
},
|
|
1009
|
+
"needsPlayFunction": false
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"name": "Loading",
|
|
1013
|
+
"description": "Loading state.",
|
|
1014
|
+
"args": {
|
|
1015
|
+
"posts": [],
|
|
1016
|
+
"isLoading": true,
|
|
1017
|
+
"error": null,
|
|
1018
|
+
"hasMore": false,
|
|
1019
|
+
"onLoadMore": "fn()",
|
|
1020
|
+
"onRetry": "fn()",
|
|
1021
|
+
"onRefresh": "fn()"
|
|
1022
|
+
},
|
|
1023
|
+
"needsPlayFunction": false
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"name": "Error",
|
|
1027
|
+
"description": "Error state.",
|
|
1028
|
+
"args": {
|
|
1029
|
+
"posts": [],
|
|
1030
|
+
"isLoading": false,
|
|
1031
|
+
"error": "Failed to load posts",
|
|
1032
|
+
"hasMore": false,
|
|
1033
|
+
"onLoadMore": "fn()",
|
|
1034
|
+
"onRetry": "fn()",
|
|
1035
|
+
"onRefresh": "fn()"
|
|
1036
|
+
},
|
|
1037
|
+
"needsPlayFunction": true,
|
|
1038
|
+
"playDescription": "1. Click retry button using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"name": "Empty",
|
|
1042
|
+
"description": "Empty state.",
|
|
1043
|
+
"args": {
|
|
1044
|
+
"posts": [],
|
|
1045
|
+
"isLoading": false,
|
|
1046
|
+
"error": null,
|
|
1047
|
+
"hasMore": false,
|
|
1048
|
+
"onLoadMore": "fn()",
|
|
1049
|
+
"onRetry": "fn()",
|
|
1050
|
+
"onRefresh": "fn()"
|
|
1051
|
+
},
|
|
1052
|
+
"needsPlayFunction": false
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
"name": "WithMore",
|
|
1056
|
+
"description": "With more posts available.",
|
|
1057
|
+
"args": {
|
|
1058
|
+
"posts": [
|
|
1059
|
+
{
|
|
1060
|
+
"id": "1",
|
|
1061
|
+
"user": {
|
|
1062
|
+
"name": "User1",
|
|
1063
|
+
"avatar": "avatar1"
|
|
1064
|
+
},
|
|
1065
|
+
"photoUrl": "photo1",
|
|
1066
|
+
"caption": "Cap1",
|
|
1067
|
+
"likeCount": 10,
|
|
1068
|
+
"commentCount": 2
|
|
1069
|
+
}
|
|
1070
|
+
],
|
|
1071
|
+
"isLoading": false,
|
|
1072
|
+
"error": null,
|
|
1073
|
+
"hasMore": true,
|
|
1074
|
+
"onLoadMore": "fn()",
|
|
1075
|
+
"onRetry": "fn()",
|
|
1076
|
+
"onRefresh": "fn()"
|
|
1077
|
+
},
|
|
1078
|
+
"needsPlayFunction": true,
|
|
1079
|
+
"playDescription": "1. Scroll to bottom using userEvent.keyboard('{ArrowDown}'), 2. Await waitFor(() => expect(onLoadMore).toHaveBeenCalled())"
|
|
1080
|
+
}
|
|
1081
|
+
],
|
|
1082
|
+
"dataContract": {
|
|
1083
|
+
"source": "props",
|
|
1084
|
+
"propsFieldName": "posts",
|
|
1085
|
+
"fields": [
|
|
1086
|
+
"posts[].id",
|
|
1087
|
+
"posts[].user.name",
|
|
1088
|
+
"posts[].user.avatar",
|
|
1089
|
+
"posts[].photoUrl",
|
|
1090
|
+
"posts[].caption",
|
|
1091
|
+
"posts[].likeCount",
|
|
1092
|
+
"posts[].commentCount"
|
|
1093
|
+
]
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
"componentId": "feed-stream",
|
|
1098
|
+
"componentName": "FeedStream",
|
|
1099
|
+
"isNew": true,
|
|
1100
|
+
"atomicType": "organism",
|
|
1101
|
+
"composes": ["post-card", "ui-components-scrollarea"],
|
|
1102
|
+
"specDeltas": {
|
|
1103
|
+
"structure": ["Similar to PostsList but for feed, with infinite scroll", "Renders PostCard for each feed item"],
|
|
1104
|
+
"rendering": [
|
|
1105
|
+
"In ready state, renders feedItems as PostCard list",
|
|
1106
|
+
"In loading state, renders Skeleton placeholders",
|
|
1107
|
+
"In error state, renders Alert with retry",
|
|
1108
|
+
"In empty state, renders 'Follow users to see posts' message",
|
|
1109
|
+
"Supports pull-to-refresh indicator"
|
|
1110
|
+
],
|
|
1111
|
+
"interaction": [
|
|
1112
|
+
"Pull-to-refresh gesture calls onRefresh: () => void",
|
|
1113
|
+
"Infinite scroll calls onLoadMore: () => void"
|
|
1114
|
+
],
|
|
1115
|
+
"styling": [
|
|
1116
|
+
"ScrollArea uses `h-full w-full overflow-y-auto` with scrollbar `w-2 thumb:bg-border`",
|
|
1117
|
+
"Stream uses `flex flex-col gap-6 p-4 max-w-2xl mx-auto` for centered feed",
|
|
1118
|
+
"Load more trigger uses `h-px w-full`",
|
|
1119
|
+
"Error banner uses `variant=\"destructive\" p-4 m-4 rounded-md` with retry `variant=\"default\"`",
|
|
1120
|
+
"Empty message uses `text-center py-12 text-lg text-muted-foreground` with icon `h-16 w-16 mb-4`",
|
|
1121
|
+
"Skeleton stream uses 10 items each `h-96 w-full bg-muted animate-pulse rounded-lg`",
|
|
1122
|
+
"Pull-to-refresh uses `absolute top-0 inset-x-0 flex justify-center py-4 bg-background` with progress bar `h-1 bg-primary` animating width via `useSpring({ width: `${pullProgress}%`, config: { tension: 120, friction: 14 } })`",
|
|
1123
|
+
"Item animation uses `useTransition(feedItems, { from: { opacity: 0, transform: 'translateY(20px)' }, enter: { opacity: 1, transform: 'translateY(0)' }, leave: { opacity: 0, transform: 'translateY(-20px)' }, config: { tension: 300, friction: 22 } })`, respects `useReducedMotion()`",
|
|
1124
|
+
"Refreshing spinner uses `animate-spin h-8 w-8 text-primary` in pull area",
|
|
1125
|
+
"Touch targets use `touch-action: manipulation` for refresh gesture",
|
|
1126
|
+
"Dark mode adjusts skeletons to `bg-muted`"
|
|
1127
|
+
]
|
|
1128
|
+
},
|
|
1129
|
+
"props": [
|
|
1130
|
+
{
|
|
1131
|
+
"name": "feedItems",
|
|
1132
|
+
"type": "Array<{ id: string; user: { name: string; avatar: string }; photoUrl: string; caption: string; likeCount: number; commentCount: number }>",
|
|
1133
|
+
"required": true,
|
|
1134
|
+
"description": "Feed items.",
|
|
1135
|
+
"category": "data"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
"name": "isLoading",
|
|
1139
|
+
"type": "boolean",
|
|
1140
|
+
"required": false,
|
|
1141
|
+
"default": "false",
|
|
1142
|
+
"description": "Loading state.",
|
|
1143
|
+
"category": "state"
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
"name": "error",
|
|
1147
|
+
"type": "string | null",
|
|
1148
|
+
"required": false,
|
|
1149
|
+
"default": "null",
|
|
1150
|
+
"description": "Error message.",
|
|
1151
|
+
"category": "state"
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
"name": "hasMore",
|
|
1155
|
+
"type": "boolean",
|
|
1156
|
+
"required": false,
|
|
1157
|
+
"default": "false",
|
|
1158
|
+
"description": "More items available.",
|
|
1159
|
+
"category": "state"
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
"name": "onLoadMore",
|
|
1163
|
+
"type": "() => void",
|
|
1164
|
+
"required": false,
|
|
1165
|
+
"description": "Load more callback.",
|
|
1166
|
+
"category": "callback"
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
"name": "onRefresh",
|
|
1170
|
+
"type": "() => void",
|
|
1171
|
+
"required": false,
|
|
1172
|
+
"description": "Refresh callback.",
|
|
1173
|
+
"category": "callback"
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
"name": "onRetry",
|
|
1177
|
+
"type": "() => void",
|
|
1178
|
+
"required": false,
|
|
1179
|
+
"description": "Retry callback.",
|
|
1180
|
+
"category": "callback"
|
|
1181
|
+
}
|
|
1182
|
+
],
|
|
1183
|
+
"storyVariants": [
|
|
1184
|
+
{
|
|
1185
|
+
"name": "Default",
|
|
1186
|
+
"description": "Feed with items.",
|
|
1187
|
+
"args": {
|
|
1188
|
+
"feedItems": [
|
|
1189
|
+
{
|
|
1190
|
+
"id": "1",
|
|
1191
|
+
"user": {
|
|
1192
|
+
"name": "User1",
|
|
1193
|
+
"avatar": "avatar1"
|
|
1194
|
+
},
|
|
1195
|
+
"photoUrl": "photo1",
|
|
1196
|
+
"caption": "Cap1",
|
|
1197
|
+
"likeCount": 10,
|
|
1198
|
+
"commentCount": 2
|
|
1199
|
+
}
|
|
1200
|
+
],
|
|
1201
|
+
"isLoading": false,
|
|
1202
|
+
"error": null,
|
|
1203
|
+
"hasMore": false,
|
|
1204
|
+
"onLoadMore": "fn()",
|
|
1205
|
+
"onRefresh": "fn()",
|
|
1206
|
+
"onRetry": "fn()"
|
|
1207
|
+
},
|
|
1208
|
+
"needsPlayFunction": false
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
"name": "Loading",
|
|
1212
|
+
"description": "Loading state.",
|
|
1213
|
+
"args": {
|
|
1214
|
+
"feedItems": [],
|
|
1215
|
+
"isLoading": true,
|
|
1216
|
+
"error": null,
|
|
1217
|
+
"hasMore": false,
|
|
1218
|
+
"onLoadMore": "fn()",
|
|
1219
|
+
"onRefresh": "fn()",
|
|
1220
|
+
"onRetry": "fn()"
|
|
1221
|
+
},
|
|
1222
|
+
"needsPlayFunction": false
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
"name": "Error",
|
|
1226
|
+
"description": "Error state.",
|
|
1227
|
+
"args": {
|
|
1228
|
+
"feedItems": [],
|
|
1229
|
+
"isLoading": false,
|
|
1230
|
+
"error": "Failed to load feed",
|
|
1231
|
+
"hasMore": false,
|
|
1232
|
+
"onLoadMore": "fn()",
|
|
1233
|
+
"onRefresh": "fn()",
|
|
1234
|
+
"onRetry": "fn()"
|
|
1235
|
+
},
|
|
1236
|
+
"needsPlayFunction": true,
|
|
1237
|
+
"playDescription": "1. Click retry using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
"name": "Empty",
|
|
1241
|
+
"description": "Empty feed.",
|
|
1242
|
+
"args": {
|
|
1243
|
+
"feedItems": [],
|
|
1244
|
+
"isLoading": false,
|
|
1245
|
+
"error": null,
|
|
1246
|
+
"hasMore": false,
|
|
1247
|
+
"onLoadMore": "fn()",
|
|
1248
|
+
"onRefresh": "fn()",
|
|
1249
|
+
"onRetry": "fn()"
|
|
1250
|
+
},
|
|
1251
|
+
"needsPlayFunction": false
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
"dataContract": {
|
|
1255
|
+
"source": "props",
|
|
1256
|
+
"propsFieldName": "feedItems",
|
|
1257
|
+
"fields": [
|
|
1258
|
+
"feedItems[].id",
|
|
1259
|
+
"feedItems[].user.name",
|
|
1260
|
+
"feedItems[].user.avatar",
|
|
1261
|
+
"feedItems[].photoUrl",
|
|
1262
|
+
"feedItems[].caption",
|
|
1263
|
+
"feedItems[].likeCount",
|
|
1264
|
+
"feedItems[].commentCount"
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
"componentId": "follow-suggestions",
|
|
1270
|
+
"componentName": "FollowSuggestions",
|
|
1271
|
+
"isNew": true,
|
|
1272
|
+
"atomicType": "organism",
|
|
1273
|
+
"composes": ["ui-components-avatar", "ui-components-button", "ui-components-carousel"],
|
|
1274
|
+
"specDeltas": {
|
|
1275
|
+
"structure": ["Uses Carousel for suggested users", "Each slide as Card with Avatar, name, Follow Button"],
|
|
1276
|
+
"rendering": [
|
|
1277
|
+
"Renders users prop as carousel items",
|
|
1278
|
+
"If isLoading=true, renders Skeleton cards",
|
|
1279
|
+
"If error, renders Alert"
|
|
1280
|
+
],
|
|
1281
|
+
"interaction": [
|
|
1282
|
+
"Clicking Follow Button calls onFollow: (userId: string) => void",
|
|
1283
|
+
"Button disables during follow action"
|
|
1284
|
+
],
|
|
1285
|
+
"styling": [
|
|
1286
|
+
"Carousel uses `w-full` with content `flex gap-4 overflow-x-auto pb-4`",
|
|
1287
|
+
"Each card uses `min-w-[200px] rounded-lg border bg-card p-4 flex flex-col items-center gap-2 shadow-sm`",
|
|
1288
|
+
"Avatar uses `h-16 w-16 rounded-full`",
|
|
1289
|
+
"Name uses `text-sm font-semibold`",
|
|
1290
|
+
"Follow button uses `variant=\"default\" size=\"sm\" mt-2` rendering `bg-primary text-primary-foreground h-9 px-4 rounded-md`",
|
|
1291
|
+
"Skeleton card uses `h-48 w-48 bg-muted animate-pulse rounded-lg`",
|
|
1292
|
+
"Error alert uses `variant=\"destructive\" p-4 rounded-md`",
|
|
1293
|
+
"Carousel navigation buttons use `variant=\"outline\" size=\"icon\" absolute top-1/2` with chevron icons `h-4 w-4`",
|
|
1294
|
+
"Slide animation uses `useTransition(true, { from: { transform: 'translateX(100%)' }, enter: { transform: 'translateX(0%)' }, config: { tension: 280, friction: 24 } })` for smooth enter, respects `useReducedMotion()`",
|
|
1295
|
+
"Hover on card wrapped in `@media (hover: hover) and (pointer: fine) { hover:shadow-md }` with CSS `transition: box-shadow 150ms ease`",
|
|
1296
|
+
"Touch targets for buttons use `min-h-11 min-w-11 touch-action: manipulation`",
|
|
1297
|
+
"Dark mode uses `bg-card text-card-foreground border-border` for cards"
|
|
1298
|
+
]
|
|
1299
|
+
},
|
|
1300
|
+
"props": [
|
|
1301
|
+
{
|
|
1302
|
+
"name": "users",
|
|
1303
|
+
"type": "Array<{ id: string; name: string; avatar: string }>",
|
|
1304
|
+
"required": true,
|
|
1305
|
+
"description": "Suggested users.",
|
|
1306
|
+
"category": "data"
|
|
1307
|
+
},
|
|
1308
|
+
{
|
|
1309
|
+
"name": "isLoading",
|
|
1310
|
+
"type": "boolean",
|
|
1311
|
+
"required": false,
|
|
1312
|
+
"default": "false",
|
|
1313
|
+
"description": "Loading state.",
|
|
1314
|
+
"category": "state"
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
"name": "error",
|
|
1318
|
+
"type": "string | null",
|
|
1319
|
+
"required": false,
|
|
1320
|
+
"default": "null",
|
|
1321
|
+
"description": "Error message.",
|
|
1322
|
+
"category": "state"
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
"name": "onFollow",
|
|
1326
|
+
"type": "(userId: string) => void",
|
|
1327
|
+
"required": true,
|
|
1328
|
+
"description": "Callback to follow user.",
|
|
1329
|
+
"category": "callback"
|
|
1330
|
+
}
|
|
1331
|
+
],
|
|
1332
|
+
"storyVariants": [
|
|
1333
|
+
{
|
|
1334
|
+
"name": "Default",
|
|
1335
|
+
"description": "With suggestions.",
|
|
1336
|
+
"args": {
|
|
1337
|
+
"users": [
|
|
1338
|
+
{
|
|
1339
|
+
"id": "1",
|
|
1340
|
+
"name": "User1",
|
|
1341
|
+
"avatar": "avatar1"
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
"id": "2",
|
|
1345
|
+
"name": "User2",
|
|
1346
|
+
"avatar": "avatar2"
|
|
1347
|
+
}
|
|
1348
|
+
],
|
|
1349
|
+
"isLoading": false,
|
|
1350
|
+
"error": null,
|
|
1351
|
+
"onFollow": "fn()"
|
|
1352
|
+
},
|
|
1353
|
+
"needsPlayFunction": true,
|
|
1354
|
+
"playDescription": "1. Click follow button for first user using userEvent.click(getByRole('button', { name: /Follow User1/ })), 2. Assert onFollow called with '1'"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
"name": "Loading",
|
|
1358
|
+
"description": "Loading state.",
|
|
1359
|
+
"args": {
|
|
1360
|
+
"users": [],
|
|
1361
|
+
"isLoading": true,
|
|
1362
|
+
"error": null,
|
|
1363
|
+
"onFollow": "fn()"
|
|
1364
|
+
},
|
|
1365
|
+
"needsPlayFunction": false
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
"name": "Error",
|
|
1369
|
+
"description": "Error state.",
|
|
1370
|
+
"args": {
|
|
1371
|
+
"users": [],
|
|
1372
|
+
"isLoading": false,
|
|
1373
|
+
"error": "Failed to load suggestions",
|
|
1374
|
+
"onFollow": "fn()"
|
|
1375
|
+
},
|
|
1376
|
+
"needsPlayFunction": false
|
|
1377
|
+
}
|
|
1378
|
+
],
|
|
1379
|
+
"dataContract": {
|
|
1380
|
+
"source": "props",
|
|
1381
|
+
"propsFieldName": "users",
|
|
1382
|
+
"fields": ["users[].id", "users[].name", "users[].avatar"]
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
"componentId": "post-form",
|
|
1387
|
+
"componentName": "PostForm",
|
|
1388
|
+
"isNew": true,
|
|
1389
|
+
"atomicType": "organism",
|
|
1390
|
+
"composes": ["photo-upload", "caption-input", "ui-components-button", "ui-components-alertdialog"],
|
|
1391
|
+
"specDeltas": {
|
|
1392
|
+
"structure": [
|
|
1393
|
+
"Vertical stack with PhotoUpload, CaptionInput, submit Button",
|
|
1394
|
+
"Uses AlertDialog for unsaved changes confirmation"
|
|
1395
|
+
],
|
|
1396
|
+
"rendering": [
|
|
1397
|
+
"In editing state, renders form fields and submit Button",
|
|
1398
|
+
"In submitting state, disables fields and Button, shows progress spinner",
|
|
1399
|
+
"In error state, shows Alert with error message and retry Button",
|
|
1400
|
+
"In invalid state, shows inline errors from child components"
|
|
1401
|
+
],
|
|
1402
|
+
"interaction": [
|
|
1403
|
+
"On submit Button click, validates form, calls onSubmit: (data: { photo: File; caption: string }) => void if valid",
|
|
1404
|
+
"On cancel, if form dirty, shows confirmation AlertDialog; on confirm calls onCancel: () => void",
|
|
1405
|
+
"Handles photo upload via PhotoUpload's onUpload",
|
|
1406
|
+
"Validates caption not empty before submit"
|
|
1407
|
+
],
|
|
1408
|
+
"styling": [
|
|
1409
|
+
"Form wrapper uses `flex flex-col gap-6 p-6 bg-background rounded-lg shadow-sm`",
|
|
1410
|
+
"Submit button uses `variant=\"default\"` rendering `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md`",
|
|
1411
|
+
"Cancel button uses `variant=\"ghost\"` rendering `bg-transparent hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2`",
|
|
1412
|
+
"Progress spinner during submitting uses `animate-spin h-6 w-6 text-primary mx-auto`",
|
|
1413
|
+
"Error alert uses `variant=\"destructive\" p-4 rounded-md mb-4` with retry button `variant=\"outline\" mt-2`",
|
|
1414
|
+
"Confirmation dialog uses modal `useTransition(isOpen, { overlay: { opacity: 0 to 1 }, content: { opacity: 0 to 1, scale: 0.95 to 1 }, config: { tension: 300, friction: 22 } })`, overlay and content share config, respects `useReducedMotion()` with `immediate: true`",
|
|
1415
|
+
"Dialog content uses `bg-background p-6 rounded-lg shadow-xl max-w-md` with z-[400]",
|
|
1416
|
+
"Dialog title uses `text-lg font-semibold`, description `text-sm text-muted-foreground`",
|
|
1417
|
+
"Dialog buttons use `variant=\"destructive\" for discard, variant=\"ghost\" for cancel`",
|
|
1418
|
+
"Hover on buttons wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }` with CSS `transition: background-color 150ms ease`",
|
|
1419
|
+
"Touch targets use `min-h-11 touch-action: manipulation` for buttons",
|
|
1420
|
+
"Skeleton for form uses PhotoUpload skeleton `h-64`, CaptionInput `h-20`, button `h-10 w-32 bg-muted animate-pulse`",
|
|
1421
|
+
"Dark mode uses `bg-background text-foreground`"
|
|
1422
|
+
]
|
|
1423
|
+
},
|
|
1424
|
+
"props": [
|
|
1425
|
+
{
|
|
1426
|
+
"name": "isSubmitting",
|
|
1427
|
+
"type": "boolean",
|
|
1428
|
+
"required": false,
|
|
1429
|
+
"default": "false",
|
|
1430
|
+
"description": "Submitting state.",
|
|
1431
|
+
"category": "state"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
"name": "error",
|
|
1435
|
+
"type": "string | null",
|
|
1436
|
+
"required": false,
|
|
1437
|
+
"default": "null",
|
|
1438
|
+
"description": "Submission error.",
|
|
1439
|
+
"category": "state"
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
"name": "onSubmit",
|
|
1443
|
+
"type": "(data: { photo: File; caption: string }) => void",
|
|
1444
|
+
"required": true,
|
|
1445
|
+
"description": "Submit callback.",
|
|
1446
|
+
"category": "callback"
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
"name": "onCancel",
|
|
1450
|
+
"type": "() => void",
|
|
1451
|
+
"required": true,
|
|
1452
|
+
"description": "Cancel callback.",
|
|
1453
|
+
"category": "callback"
|
|
1454
|
+
}
|
|
1455
|
+
],
|
|
1456
|
+
"storyVariants": [
|
|
1457
|
+
{
|
|
1458
|
+
"name": "Default",
|
|
1459
|
+
"description": "Editing form.",
|
|
1460
|
+
"args": {
|
|
1461
|
+
"isSubmitting": false,
|
|
1462
|
+
"error": null,
|
|
1463
|
+
"onSubmit": "fn()",
|
|
1464
|
+
"onCancel": "fn()"
|
|
1465
|
+
},
|
|
1466
|
+
"needsPlayFunction": false
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
"name": "Submitting",
|
|
1470
|
+
"description": "Submitting state.",
|
|
1471
|
+
"args": {
|
|
1472
|
+
"isSubmitting": true,
|
|
1473
|
+
"error": null,
|
|
1474
|
+
"onSubmit": "fn()",
|
|
1475
|
+
"onCancel": "fn()"
|
|
1476
|
+
},
|
|
1477
|
+
"needsPlayFunction": false
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
"name": "Error",
|
|
1481
|
+
"description": "Error state.",
|
|
1482
|
+
"args": {
|
|
1483
|
+
"isSubmitting": false,
|
|
1484
|
+
"error": "Failed to create post",
|
|
1485
|
+
"onSubmit": "fn()",
|
|
1486
|
+
"onCancel": "fn()"
|
|
1487
|
+
},
|
|
1488
|
+
"needsPlayFunction": false
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
"name": "Interactive",
|
|
1492
|
+
"description": "For testing submit.",
|
|
1493
|
+
"args": {
|
|
1494
|
+
"isSubmitting": false,
|
|
1495
|
+
"error": null,
|
|
1496
|
+
"onSubmit": "fn()",
|
|
1497
|
+
"onCancel": "fn()"
|
|
1498
|
+
},
|
|
1499
|
+
"needsPlayFunction": true,
|
|
1500
|
+
"playDescription": "1. Simulate photo upload and caption input, 2. Click submit using userEvent.click(getByRole('button', { name: 'Post' })), 3. Assert onSubmit called with data"
|
|
1501
|
+
}
|
|
1502
|
+
],
|
|
1503
|
+
"dataContract": {
|
|
1504
|
+
"source": "local-state",
|
|
1505
|
+
"fields": ["photo", "caption"]
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
"componentId": "post-detail-media",
|
|
1510
|
+
"componentName": "PostDetailMedia",
|
|
1511
|
+
"isNew": true,
|
|
1512
|
+
"atomicType": "organism",
|
|
1513
|
+
"composes": ["ui-components-aspectratio"],
|
|
1514
|
+
"specDeltas": {
|
|
1515
|
+
"structure": ["Uses AspectRatio for full-size media display", "img element with loading='lazy'"],
|
|
1516
|
+
"rendering": [
|
|
1517
|
+
"Renders img with src=photoUrl, alt='Post media'",
|
|
1518
|
+
"If isLoading=true, renders Skeleton in AspectRatio",
|
|
1519
|
+
"If error, renders Alert"
|
|
1520
|
+
],
|
|
1521
|
+
"interaction": ["No interactions"],
|
|
1522
|
+
"styling": [
|
|
1523
|
+
"AspectRatio uses `aspect-[4/3]` with img `object-cover w-full h-full rounded-lg`",
|
|
1524
|
+
"Skeleton uses `h-[500px] w-full bg-muted animate-pulse rounded-lg` to reserve space",
|
|
1525
|
+
"Error alert uses `variant=\"destructive\" p-4 rounded-md h-[500px] flex items-center justify-center`",
|
|
1526
|
+
"Enter animation for img uses `useSpring({ opacity: 1, from: { opacity: 0 }, config: { tension: 400, friction: 26 } })`, respects `useReducedMotion()`",
|
|
1527
|
+
"Img has `pointer-events: none` as decorative",
|
|
1528
|
+
"Focus visible not applicable, but container uses `focus-visible:ring-2 ring-ring ring-offset-2` if interactive",
|
|
1529
|
+
"Dark mode no change needed",
|
|
1530
|
+
"Use `aspect-ratio: 4/3` on container to prevent shift",
|
|
1531
|
+
"Subtle shadow uses `box-shadow: 0 0 0 1px rgba(0,0,0,0.08)`",
|
|
1532
|
+
"Hover no effect, but if added wrap in media query",
|
|
1533
|
+
"Touch ready not applicable"
|
|
1534
|
+
]
|
|
1535
|
+
},
|
|
1536
|
+
"props": [
|
|
1537
|
+
{
|
|
1538
|
+
"name": "photoUrl",
|
|
1539
|
+
"type": "string",
|
|
1540
|
+
"required": true,
|
|
1541
|
+
"description": "Media URL.",
|
|
1542
|
+
"category": "data"
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"name": "isLoading",
|
|
1546
|
+
"type": "boolean",
|
|
1547
|
+
"required": false,
|
|
1548
|
+
"default": "false",
|
|
1549
|
+
"description": "Loading state.",
|
|
1550
|
+
"category": "state"
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
"name": "error",
|
|
1554
|
+
"type": "string | null",
|
|
1555
|
+
"required": false,
|
|
1556
|
+
"default": "null",
|
|
1557
|
+
"description": "Error message.",
|
|
1558
|
+
"category": "state"
|
|
1559
|
+
}
|
|
1560
|
+
],
|
|
1561
|
+
"storyVariants": [
|
|
1562
|
+
{
|
|
1563
|
+
"name": "Default",
|
|
1564
|
+
"description": "With media.",
|
|
1565
|
+
"args": {
|
|
1566
|
+
"photoUrl": "https://example.com/photo.jpg",
|
|
1567
|
+
"isLoading": false,
|
|
1568
|
+
"error": null
|
|
1569
|
+
},
|
|
1570
|
+
"needsPlayFunction": false
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
"name": "Loading",
|
|
1574
|
+
"description": "Loading state.",
|
|
1575
|
+
"args": {
|
|
1576
|
+
"photoUrl": "",
|
|
1577
|
+
"isLoading": true,
|
|
1578
|
+
"error": null
|
|
1579
|
+
},
|
|
1580
|
+
"needsPlayFunction": false
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
"name": "Error",
|
|
1584
|
+
"description": "Error state.",
|
|
1585
|
+
"args": {
|
|
1586
|
+
"photoUrl": "",
|
|
1587
|
+
"isLoading": false,
|
|
1588
|
+
"error": "Failed to load media"
|
|
1589
|
+
},
|
|
1590
|
+
"needsPlayFunction": false
|
|
1591
|
+
}
|
|
1592
|
+
],
|
|
1593
|
+
"dataContract": {
|
|
1594
|
+
"source": "props",
|
|
1595
|
+
"propsFieldName": "photoUrl",
|
|
1596
|
+
"fields": ["photoUrl"]
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
"componentId": "post-actions",
|
|
1601
|
+
"componentName": "PostActions",
|
|
1602
|
+
"isNew": true,
|
|
1603
|
+
"atomicType": "organism",
|
|
1604
|
+
"composes": ["like-button", "ui-components-button"],
|
|
1605
|
+
"specDeltas": {
|
|
1606
|
+
"structure": ["Horizontal group with LikeButton, comment Button, share Button"],
|
|
1607
|
+
"rendering": [
|
|
1608
|
+
"Renders LikeButton with current like state and count",
|
|
1609
|
+
"Renders comment Button with comment count",
|
|
1610
|
+
"Renders share Button"
|
|
1611
|
+
],
|
|
1612
|
+
"interaction": [
|
|
1613
|
+
"LikeButton handles toggle",
|
|
1614
|
+
"Comment Button calls onComment: () => void to focus input",
|
|
1615
|
+
"Share Button calls onShare: (postId: string) => void"
|
|
1616
|
+
],
|
|
1617
|
+
"styling": [
|
|
1618
|
+
"Wrapper uses `flex justify-around border-t border-border py-4 bg-background`",
|
|
1619
|
+
"Each action uses `flex flex-col items-center gap-1 text-muted-foreground`",
|
|
1620
|
+
"Icons use `h-6 w-6`",
|
|
1621
|
+
"Counts use `text-xs font-medium` with `font-variant-numeric: tabular-nums`",
|
|
1622
|
+
"Like button when pressed uses `text-red-500` for icon",
|
|
1623
|
+
"Hover on actions wrapped in `@media (hover: hover) and (pointer: fine) { hover:text-foreground }` with CSS `transition: color 150ms ease`",
|
|
1624
|
+
"Active state for buttons uses CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`",
|
|
1625
|
+
"Focus visible uses `focus-visible:ring-2 ring-ring ring-offset-2 rounded-full p-2`",
|
|
1626
|
+
"Touch targets use `min-h-11 min-w-11 touch-action: manipulation` for each action",
|
|
1627
|
+
"Dark mode uses `bg-background text-muted-foreground border-border`",
|
|
1628
|
+
"Separator uses `box-shadow: 0 0 0 1px rgba(0,0,0,0.08)`"
|
|
1629
|
+
]
|
|
1630
|
+
},
|
|
1631
|
+
"props": [
|
|
1632
|
+
{
|
|
1633
|
+
"name": "postId",
|
|
1634
|
+
"type": "string",
|
|
1635
|
+
"required": true,
|
|
1636
|
+
"description": "Post ID.",
|
|
1637
|
+
"category": "data"
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
"name": "liked",
|
|
1641
|
+
"type": "boolean",
|
|
1642
|
+
"required": false,
|
|
1643
|
+
"default": "false",
|
|
1644
|
+
"description": "Liked state.",
|
|
1645
|
+
"category": "state"
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
"name": "likeCount",
|
|
1649
|
+
"type": "number",
|
|
1650
|
+
"required": false,
|
|
1651
|
+
"default": "0",
|
|
1652
|
+
"description": "Like count.",
|
|
1653
|
+
"category": "data"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
"name": "commentCount",
|
|
1657
|
+
"type": "number",
|
|
1658
|
+
"required": false,
|
|
1659
|
+
"default": "0",
|
|
1660
|
+
"description": "Comment count.",
|
|
1661
|
+
"category": "data"
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
"name": "onToggleLike",
|
|
1665
|
+
"type": "(liked: boolean) => void",
|
|
1666
|
+
"required": true,
|
|
1667
|
+
"description": "Toggle like callback.",
|
|
1668
|
+
"category": "callback"
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
"name": "onComment",
|
|
1672
|
+
"type": "() => void",
|
|
1673
|
+
"required": true,
|
|
1674
|
+
"description": "Comment callback.",
|
|
1675
|
+
"category": "callback"
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
"name": "onShare",
|
|
1679
|
+
"type": "(postId: string) => void",
|
|
1680
|
+
"required": true,
|
|
1681
|
+
"description": "Share callback.",
|
|
1682
|
+
"category": "callback"
|
|
1683
|
+
}
|
|
1684
|
+
],
|
|
1685
|
+
"storyVariants": [
|
|
1686
|
+
{
|
|
1687
|
+
"name": "Default",
|
|
1688
|
+
"description": "Actions bar.",
|
|
1689
|
+
"args": {
|
|
1690
|
+
"postId": "1",
|
|
1691
|
+
"liked": false,
|
|
1692
|
+
"likeCount": 0,
|
|
1693
|
+
"commentCount": 0,
|
|
1694
|
+
"onToggleLike": "fn()",
|
|
1695
|
+
"onComment": "fn()",
|
|
1696
|
+
"onShare": "fn()"
|
|
1697
|
+
},
|
|
1698
|
+
"needsPlayFunction": true,
|
|
1699
|
+
"playDescription": "1. Click like button, assert onToggleLike called; 2. Click comment, assert onComment called; 3. Click share, assert onShare called with '1'"
|
|
1700
|
+
}
|
|
1701
|
+
],
|
|
1702
|
+
"dataContract": {
|
|
1703
|
+
"source": "props",
|
|
1704
|
+
"propsFieldName": "postId",
|
|
1705
|
+
"fields": ["postId", "liked", "likeCount", "commentCount"]
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
"componentId": "create-post-page",
|
|
1710
|
+
"componentName": "CreatePostPage",
|
|
1711
|
+
"isNew": true,
|
|
1712
|
+
"atomicType": "page",
|
|
1713
|
+
"composes": ["post-form", "post-preview", "ui-components-button"],
|
|
1714
|
+
"specDeltas": {
|
|
1715
|
+
"structure": [
|
|
1716
|
+
"Two-region layout: form-area (left/main) with PostForm, preview-area (right/secondary) with PostPreview",
|
|
1717
|
+
"Header region with Cancel Button and title 'Create Post'"
|
|
1718
|
+
],
|
|
1719
|
+
"rendering": [
|
|
1720
|
+
"In editing state, shows form-area and preview-area, hides nothing",
|
|
1721
|
+
"In editing-invalid state, shows form-area and preview-area with inline validation errors from PostForm",
|
|
1722
|
+
"In submitting state, shows form-area disabled with progress indicator in form-area, hides preview-area",
|
|
1723
|
+
"In ready state, hides form-area and preview-area, shows success toast 'Post created successfully'",
|
|
1724
|
+
"In error state, shows form-area and preview-area, shows error banner in form-area with 'Failed to create post. Please try again.' and retry Button",
|
|
1725
|
+
"In loading state, shows header, hides form-area and preview-area, shows skeleton in form-area (PhotoUpload skeleton, CaptionInput skeleton, Button skeleton) and preview-area (PostPreview skeleton)"
|
|
1726
|
+
],
|
|
1727
|
+
"interaction": [
|
|
1728
|
+
"Calls onSubmit: (data: { photo: File; caption: string }) => void from PostForm submit, transitions to submitting state",
|
|
1729
|
+
"Clicking Cancel Button, if form dirty shows confirmation AlertDialog 'Discard changes?', on confirm navigates to view-posts calling onCancel: () => void, transitions to ready if success",
|
|
1730
|
+
"On retry in error state, calls onRetry: () => void, transitions to submitting",
|
|
1731
|
+
"Escape key triggers cancel action with confirmation if dirty"
|
|
1732
|
+
],
|
|
1733
|
+
"styling": [
|
|
1734
|
+
"Page layout uses `grid grid-cols-1 md:grid-cols-2 gap-8 p-8 max-w-7xl mx-auto` for form-area and preview-area",
|
|
1735
|
+
"Header uses `col-span-full flex items-center justify-between mb-6` with title `text-2xl font-bold tracking-tight text-wrap: balance`",
|
|
1736
|
+
"Form-area uses `flex flex-col gap-6`",
|
|
1737
|
+
"Preview-area uses `sticky top-8` with z-[200]",
|
|
1738
|
+
"Submitting overlay uses `absolute inset-0 bg-background/80 flex items-center justify-center` with progress `useSpring({ width: '0%' to '100%', config: { tension: 120, friction: 14 } })` for bar `h-1 bg-primary`",
|
|
1739
|
+
"Error banner uses `variant=\"destructive\" p-4 rounded-md mb-6`",
|
|
1740
|
+
"Skeleton layout matches grid with form skeletons `h-64 bg-muted animate-pulse` etc.",
|
|
1741
|
+
"Confirmation modal uses `z-[400]` with overlay `bg-black/80` and content `bg-background p-6 rounded-lg` animation `useTransition(isOpen, { overlay: { opacity: 0 to 1 }, content: { opacity: 0 to 1, transform: 'scale(0.95)' to 'scale(1)' }, config: { tension: 300, friction: 22 } })`, same config for paired elements, respects `useReducedMotion()`",
|
|
1742
|
+
"Hover on cancel button wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`",
|
|
1743
|
+
"Touch targets for buttons use `min-h-11`",
|
|
1744
|
+
"Dark mode uses `bg-background text-foreground`",
|
|
1745
|
+
"Use `text-balance` for title"
|
|
1746
|
+
]
|
|
1747
|
+
},
|
|
1748
|
+
"props": [
|
|
1749
|
+
{
|
|
1750
|
+
"name": "isLoading",
|
|
1751
|
+
"type": "boolean",
|
|
1752
|
+
"required": false,
|
|
1753
|
+
"default": "false",
|
|
1754
|
+
"description": "Page loading state.",
|
|
1755
|
+
"category": "state"
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
"name": "isSubmitting",
|
|
1759
|
+
"type": "boolean",
|
|
1760
|
+
"required": false,
|
|
1761
|
+
"default": "false",
|
|
1762
|
+
"description": "Submitting state.",
|
|
1763
|
+
"category": "state"
|
|
1764
|
+
},
|
|
1765
|
+
{
|
|
1766
|
+
"name": "error",
|
|
1767
|
+
"type": "string | null",
|
|
1768
|
+
"required": false,
|
|
1769
|
+
"default": "null",
|
|
1770
|
+
"description": "Error message.",
|
|
1771
|
+
"category": "state"
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
"name": "onSubmit",
|
|
1775
|
+
"type": "(data: { photo: File; caption: string }) => void",
|
|
1776
|
+
"required": true,
|
|
1777
|
+
"description": "Submit callback.",
|
|
1778
|
+
"category": "callback"
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
"name": "onCancel",
|
|
1782
|
+
"type": "() => void",
|
|
1783
|
+
"required": true,
|
|
1784
|
+
"description": "Cancel callback.",
|
|
1785
|
+
"category": "callback"
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
"name": "onRetry",
|
|
1789
|
+
"type": "() => void",
|
|
1790
|
+
"required": false,
|
|
1791
|
+
"description": "Retry callback.",
|
|
1792
|
+
"category": "callback"
|
|
1793
|
+
}
|
|
1794
|
+
],
|
|
1795
|
+
"storyVariants": [
|
|
1796
|
+
{
|
|
1797
|
+
"name": "Default",
|
|
1798
|
+
"description": "Editing state.",
|
|
1799
|
+
"args": {
|
|
1800
|
+
"isLoading": false,
|
|
1801
|
+
"isSubmitting": false,
|
|
1802
|
+
"error": null,
|
|
1803
|
+
"onSubmit": "fn()",
|
|
1804
|
+
"onCancel": "fn()",
|
|
1805
|
+
"onRetry": "fn()"
|
|
1806
|
+
},
|
|
1807
|
+
"needsPlayFunction": false
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
"name": "Loading",
|
|
1811
|
+
"description": "Loading state.",
|
|
1812
|
+
"args": {
|
|
1813
|
+
"isLoading": true,
|
|
1814
|
+
"isSubmitting": false,
|
|
1815
|
+
"error": null,
|
|
1816
|
+
"onSubmit": "fn()",
|
|
1817
|
+
"onCancel": "fn()",
|
|
1818
|
+
"onRetry": "fn()"
|
|
1819
|
+
},
|
|
1820
|
+
"needsPlayFunction": false
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
"name": "Submitting",
|
|
1824
|
+
"description": "Submitting state.",
|
|
1825
|
+
"args": {
|
|
1826
|
+
"isLoading": false,
|
|
1827
|
+
"isSubmitting": true,
|
|
1828
|
+
"error": null,
|
|
1829
|
+
"onSubmit": "fn()",
|
|
1830
|
+
"onCancel": "fn()",
|
|
1831
|
+
"onRetry": "fn()"
|
|
1832
|
+
},
|
|
1833
|
+
"needsPlayFunction": false
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
"name": "Error",
|
|
1837
|
+
"description": "Error state.",
|
|
1838
|
+
"args": {
|
|
1839
|
+
"isLoading": false,
|
|
1840
|
+
"isSubmitting": false,
|
|
1841
|
+
"error": "Failed to create post",
|
|
1842
|
+
"onSubmit": "fn()",
|
|
1843
|
+
"onCancel": "fn()",
|
|
1844
|
+
"onRetry": "fn()"
|
|
1845
|
+
},
|
|
1846
|
+
"needsPlayFunction": true,
|
|
1847
|
+
"playDescription": "1. Click retry button using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
1848
|
+
}
|
|
1849
|
+
],
|
|
1850
|
+
"dataContract": {
|
|
1851
|
+
"source": "graphql-mutation",
|
|
1852
|
+
"operationName": "CreatePost",
|
|
1853
|
+
"fields": ["photo", "caption"]
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
"componentId": "view-posts-page",
|
|
1858
|
+
"componentName": "ViewPostsPage",
|
|
1859
|
+
"isNew": true,
|
|
1860
|
+
"atomicType": "page",
|
|
1861
|
+
"composes": ["posts-list", "ui-components-button"],
|
|
1862
|
+
"specDeltas": {
|
|
1863
|
+
"structure": [
|
|
1864
|
+
"Main region with PostsList, floating create Button in create-fab region, header with title 'My Posts'"
|
|
1865
|
+
],
|
|
1866
|
+
"rendering": [
|
|
1867
|
+
"In loading state, shows header, hides posts-list and create-fab, shows skeleton in posts-list region (5 PostCard skeletons)",
|
|
1868
|
+
"In ready state, shows header, posts-list, create-fab",
|
|
1869
|
+
"In error state, shows header, hides posts-list and create-fab, shows error banner in posts-list region with 'Failed to load posts' and retry Button",
|
|
1870
|
+
"In empty state, shows header, create-fab, shows empty message 'No posts yet. Create your first post!' in posts-list region"
|
|
1871
|
+
],
|
|
1872
|
+
"interaction": [
|
|
1873
|
+
"Clicking create Button navigates to create-post, calls onCreate: () => void",
|
|
1874
|
+
"Clicking refresh Button in ready state transitions to loading, calls onRefresh: () => void",
|
|
1875
|
+
"On retry in error state calls onRetry: () => void, transitions to loading"
|
|
1876
|
+
],
|
|
1877
|
+
"styling": [
|
|
1878
|
+
"Page uses `flex flex-col h-full`",
|
|
1879
|
+
"Header uses `flex items-center justify-between p-6 border-b bg-background` with title `text-2xl font-bold tracking-tight` and refresh button `variant=\"ghost\" size=\"icon\"`",
|
|
1880
|
+
"Posts-list region uses `flex-1 overflow-auto`",
|
|
1881
|
+
"Create fab uses `fixed bottom-6 right-6 z-[500]` with button `variant=\"default\" size=\"lg\" rounded-full shadow-lg` animation `useSpring({ scale: 1, config: { tension: 400, friction: 22 } })` on hover",
|
|
1882
|
+
"Error banner uses `p-6 text-center` with retry `variant=\"default\" mt-4`",
|
|
1883
|
+
"Empty message uses `flex flex-col items-center justify-center h-full text-muted-foreground` with icon `h-16 w-16 mb-4`, text `text-lg`",
|
|
1884
|
+
"Skeleton uses full height with grid skeletons",
|
|
1885
|
+
"Hover on refresh wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`",
|
|
1886
|
+
"Touch targets for fab use `h-14 w-14 min-h-11 touch-action: manipulation`",
|
|
1887
|
+
"Fab press animation uses CSS `active:scale-[0.97] transition: transform 100ms ease-out`",
|
|
1888
|
+
"Dark mode uses `bg-background border-border`",
|
|
1889
|
+
"Z-index for fab 500, header 200"
|
|
1890
|
+
]
|
|
1891
|
+
},
|
|
1892
|
+
"props": [
|
|
1893
|
+
{
|
|
1894
|
+
"name": "isLoading",
|
|
1895
|
+
"type": "boolean",
|
|
1896
|
+
"required": false,
|
|
1897
|
+
"default": "false",
|
|
1898
|
+
"description": "Loading state.",
|
|
1899
|
+
"category": "state"
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
"name": "error",
|
|
1903
|
+
"type": "string | null",
|
|
1904
|
+
"required": false,
|
|
1905
|
+
"default": "null",
|
|
1906
|
+
"description": "Error message.",
|
|
1907
|
+
"category": "state"
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
"name": "isEmpty",
|
|
1911
|
+
"type": "boolean",
|
|
1912
|
+
"required": false,
|
|
1913
|
+
"default": "false",
|
|
1914
|
+
"description": "Empty state.",
|
|
1915
|
+
"category": "state"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
"name": "onCreate",
|
|
1919
|
+
"type": "() => void",
|
|
1920
|
+
"required": true,
|
|
1921
|
+
"description": "Create post callback.",
|
|
1922
|
+
"category": "callback"
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
"name": "onRefresh",
|
|
1926
|
+
"type": "() => void",
|
|
1927
|
+
"required": true,
|
|
1928
|
+
"description": "Refresh callback.",
|
|
1929
|
+
"category": "callback"
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
"name": "onRetry",
|
|
1933
|
+
"type": "() => void",
|
|
1934
|
+
"required": false,
|
|
1935
|
+
"description": "Retry callback.",
|
|
1936
|
+
"category": "callback"
|
|
1937
|
+
}
|
|
1938
|
+
],
|
|
1939
|
+
"storyVariants": [
|
|
1940
|
+
{
|
|
1941
|
+
"name": "Default",
|
|
1942
|
+
"description": "Ready state.",
|
|
1943
|
+
"args": {
|
|
1944
|
+
"isLoading": false,
|
|
1945
|
+
"error": null,
|
|
1946
|
+
"isEmpty": false,
|
|
1947
|
+
"onCreate": "fn()",
|
|
1948
|
+
"onRefresh": "fn()",
|
|
1949
|
+
"onRetry": "fn()"
|
|
1950
|
+
},
|
|
1951
|
+
"needsPlayFunction": false
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
"name": "Loading",
|
|
1955
|
+
"description": "Loading state.",
|
|
1956
|
+
"args": {
|
|
1957
|
+
"isLoading": true,
|
|
1958
|
+
"error": null,
|
|
1959
|
+
"isEmpty": false,
|
|
1960
|
+
"onCreate": "fn()",
|
|
1961
|
+
"onRefresh": "fn()",
|
|
1962
|
+
"onRetry": "fn()"
|
|
1963
|
+
},
|
|
1964
|
+
"needsPlayFunction": false
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"name": "Error",
|
|
1968
|
+
"description": "Error state.",
|
|
1969
|
+
"args": {
|
|
1970
|
+
"isLoading": false,
|
|
1971
|
+
"error": "Failed to load posts",
|
|
1972
|
+
"isEmpty": false,
|
|
1973
|
+
"onCreate": "fn()",
|
|
1974
|
+
"onRefresh": "fn()",
|
|
1975
|
+
"onRetry": "fn()"
|
|
1976
|
+
},
|
|
1977
|
+
"needsPlayFunction": true,
|
|
1978
|
+
"playDescription": "1. Click retry using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
"name": "Empty",
|
|
1982
|
+
"description": "Empty state.",
|
|
1983
|
+
"args": {
|
|
1984
|
+
"isLoading": false,
|
|
1985
|
+
"error": null,
|
|
1986
|
+
"isEmpty": true,
|
|
1987
|
+
"onCreate": "fn()",
|
|
1988
|
+
"onRefresh": "fn()",
|
|
1989
|
+
"onRetry": "fn()"
|
|
1990
|
+
},
|
|
1991
|
+
"needsPlayFunction": false
|
|
1992
|
+
}
|
|
1993
|
+
],
|
|
1994
|
+
"dataContract": {
|
|
1995
|
+
"source": "graphql-query",
|
|
1996
|
+
"operationName": "GetMyPosts",
|
|
1997
|
+
"fields": ["posts[].id", "posts[].photoUrl", "posts[].caption", "posts[].likeCount", "posts[].commentCount"]
|
|
1998
|
+
}
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
"componentId": "post-detail-page",
|
|
2002
|
+
"componentName": "PostDetailPage",
|
|
2003
|
+
"isNew": true,
|
|
2004
|
+
"atomicType": "page",
|
|
2005
|
+
"composes": ["post-detail-media", "post-actions", "comments-list", "comment-input"],
|
|
2006
|
+
"specDeltas": {
|
|
2007
|
+
"structure": [
|
|
2008
|
+
"Vertical layout: header, post-media, post-actions, comments-section with CommentsList and CommentInput"
|
|
2009
|
+
],
|
|
2010
|
+
"rendering": [
|
|
2011
|
+
"In loading state, shows header, hides other regions, shows skeleton in post-media (AspectRatio skeleton), post-actions (Button skeletons), comments-section (3 comment skeletons + input skeleton)",
|
|
2012
|
+
"In ready state, shows all regions",
|
|
2013
|
+
"In error state, shows header, hides other regions, shows error banner in main region with 'Failed to load post' and retry Button"
|
|
2014
|
+
],
|
|
2015
|
+
"interaction": [
|
|
2016
|
+
"Toggle like via PostActions, calls onToggleLike: (postId: string, liked: boolean) => void, shows success toast 'Like status updated'",
|
|
2017
|
+
"Add comment via CommentInput, calls onAddComment: (postId: string, comment: string) => void, shows success toast 'Comment added' on success, inline warning if empty"
|
|
2018
|
+
],
|
|
2019
|
+
"styling": [
|
|
2020
|
+
"Page uses `flex flex-col max-w-3xl mx-auto`",
|
|
2021
|
+
"Header uses `flex items-center gap-4 p-4 border-b` with back button `variant=\"ghost\" size=\"icon\"`, metadata `text-sm text-muted-foreground`",
|
|
2022
|
+
"Post-media uses `bg-black` for full-width",
|
|
2023
|
+
"Post-actions uses `border-b py-2`",
|
|
2024
|
+
"Comments-section uses `flex-1 overflow-auto p-4`",
|
|
2025
|
+
"Loading skeleton uses media `h-[500px] bg-muted animate-pulse`, actions `h-12 bg-muted`, comments 3 `h-16 bg-muted` + input `h-10`",
|
|
2026
|
+
"Error banner uses `p-8 text-center` with retry `variant=\"default\"`",
|
|
2027
|
+
"Comment enter uses `useTransition(comments, { from: { opacity: 0, transform: 'translateY(8px)' }, enter: { opacity: 1, transform: 'translateY(0)' }, config: { tension: 300, friction: 22 } })`",
|
|
2028
|
+
"Hover on back button wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`",
|
|
2029
|
+
"Touch targets for buttons use `min-h-11`",
|
|
2030
|
+
"Dark mode uses `bg-background`",
|
|
2031
|
+
"Z-index for header 200"
|
|
2032
|
+
]
|
|
2033
|
+
},
|
|
2034
|
+
"props": [
|
|
2035
|
+
{
|
|
2036
|
+
"name": "postId",
|
|
2037
|
+
"type": "string",
|
|
2038
|
+
"required": true,
|
|
2039
|
+
"description": "Post ID from route param.",
|
|
2040
|
+
"category": "data"
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
"name": "isLoading",
|
|
2044
|
+
"type": "boolean",
|
|
2045
|
+
"required": false,
|
|
2046
|
+
"default": "false",
|
|
2047
|
+
"description": "Loading state.",
|
|
2048
|
+
"category": "state"
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
"name": "error",
|
|
2052
|
+
"type": "string | null",
|
|
2053
|
+
"required": false,
|
|
2054
|
+
"default": "null",
|
|
2055
|
+
"description": "Error message.",
|
|
2056
|
+
"category": "state"
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
"name": "onToggleLike",
|
|
2060
|
+
"type": "(postId: string, liked: boolean) => void",
|
|
2061
|
+
"required": true,
|
|
2062
|
+
"description": "Toggle like callback.",
|
|
2063
|
+
"category": "callback"
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
"name": "onAddComment",
|
|
2067
|
+
"type": "(postId: string, comment: string) => void",
|
|
2068
|
+
"required": true,
|
|
2069
|
+
"description": "Add comment callback.",
|
|
2070
|
+
"category": "callback"
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
"name": "onRetry",
|
|
2074
|
+
"type": "() => void",
|
|
2075
|
+
"required": false,
|
|
2076
|
+
"description": "Retry load callback.",
|
|
2077
|
+
"category": "callback"
|
|
2078
|
+
}
|
|
2079
|
+
],
|
|
2080
|
+
"storyVariants": [
|
|
2081
|
+
{
|
|
2082
|
+
"name": "Default",
|
|
2083
|
+
"description": "Ready state.",
|
|
2084
|
+
"args": {
|
|
2085
|
+
"postId": "1",
|
|
2086
|
+
"isLoading": false,
|
|
2087
|
+
"error": null,
|
|
2088
|
+
"onToggleLike": "fn()",
|
|
2089
|
+
"onAddComment": "fn()",
|
|
2090
|
+
"onRetry": "fn()"
|
|
2091
|
+
},
|
|
2092
|
+
"needsPlayFunction": false
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
"name": "Loading",
|
|
2096
|
+
"description": "Loading state.",
|
|
2097
|
+
"args": {
|
|
2098
|
+
"postId": "1",
|
|
2099
|
+
"isLoading": true,
|
|
2100
|
+
"error": null,
|
|
2101
|
+
"onToggleLike": "fn()",
|
|
2102
|
+
"onAddComment": "fn()",
|
|
2103
|
+
"onRetry": "fn()"
|
|
2104
|
+
},
|
|
2105
|
+
"needsPlayFunction": false
|
|
2106
|
+
},
|
|
2107
|
+
{
|
|
2108
|
+
"name": "Error",
|
|
2109
|
+
"description": "Error state.",
|
|
2110
|
+
"args": {
|
|
2111
|
+
"postId": "1",
|
|
2112
|
+
"isLoading": false,
|
|
2113
|
+
"error": "Failed to load post",
|
|
2114
|
+
"onToggleLike": "fn()",
|
|
2115
|
+
"onAddComment": "fn()",
|
|
2116
|
+
"onRetry": "fn()"
|
|
2117
|
+
},
|
|
2118
|
+
"needsPlayFunction": true,
|
|
2119
|
+
"playDescription": "1. Click retry using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
2120
|
+
}
|
|
2121
|
+
],
|
|
2122
|
+
"dataContract": {
|
|
2123
|
+
"source": "graphql-query",
|
|
2124
|
+
"operationName": "GetPostDetail",
|
|
2125
|
+
"fields": [
|
|
2126
|
+
"post.id",
|
|
2127
|
+
"post.photoUrl",
|
|
2128
|
+
"post.caption",
|
|
2129
|
+
"post.likes",
|
|
2130
|
+
"post.comments[].id",
|
|
2131
|
+
"post.comments[].user.name",
|
|
2132
|
+
"post.comments[].user.avatar",
|
|
2133
|
+
"post.comments[].text",
|
|
2134
|
+
"post.comments[].timestamp"
|
|
2135
|
+
]
|
|
2136
|
+
}
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
"componentId": "feed-page",
|
|
2140
|
+
"componentName": "FeedPage",
|
|
2141
|
+
"isNew": true,
|
|
2142
|
+
"atomicType": "page",
|
|
2143
|
+
"composes": ["feed-stream", "follow-suggestions"],
|
|
2144
|
+
"specDeltas": {
|
|
2145
|
+
"structure": [
|
|
2146
|
+
"Main feed-stream with FeedStream, secondary follow-suggestions with FollowSuggestions, header with title 'Feed'"
|
|
2147
|
+
],
|
|
2148
|
+
"rendering": [
|
|
2149
|
+
"In loading state, shows header, hides feed-stream and follow-suggestions, shows skeleton in feed-stream (10 PostCard skeletons) and follow-suggestions (3 user card skeletons)",
|
|
2150
|
+
"In ready state, shows all regions",
|
|
2151
|
+
"In error state, shows header, hides other regions, shows error banner in feed-stream with 'Failed to load feed' and retry Button",
|
|
2152
|
+
"In empty state, shows header and follow-suggestions, shows empty message 'Follow users to see posts in your feed' in feed-stream region"
|
|
2153
|
+
],
|
|
2154
|
+
"interaction": [
|
|
2155
|
+
"Refresh action transitions to loading, calls onRefresh: () => void",
|
|
2156
|
+
"Follow user via FollowSuggestions, calls onFollow: (userId: string) => void, shows success toast 'Now following user'"
|
|
2157
|
+
],
|
|
2158
|
+
"styling": [
|
|
2159
|
+
"Page uses `flex flex-col h-full`",
|
|
2160
|
+
"Header uses `p-4 border-b bg-background` with title `text-2xl font-bold`",
|
|
2161
|
+
"Feed-stream uses `flex-1`",
|
|
2162
|
+
"Follow-suggestions uses `p-4 border-t`",
|
|
2163
|
+
"Loading skeleton uses stream 10 `h-96 bg-muted`, suggestions 3 `h-48 w-48 bg-muted`",
|
|
2164
|
+
"Error banner in stream `p-8 text-center` with retry `variant=\"default\"`",
|
|
2165
|
+
"Empty message in stream `flex flex-col items-center justify-center h-full` with text `text-lg`",
|
|
2166
|
+
"Suggestions animation uses `useTransition(true, { from: { opacity: 0 }, enter: { opacity: 1 }, config: { tension: 400, friction: 26 } })`",
|
|
2167
|
+
"Hover no specific, but buttons follow media query",
|
|
2168
|
+
"Touch targets for interactions use `min-h-11`",
|
|
2169
|
+
"Dark mode uses `bg-background border-border`",
|
|
2170
|
+
"Z-index for header 200"
|
|
2171
|
+
]
|
|
2172
|
+
},
|
|
2173
|
+
"props": [
|
|
2174
|
+
{
|
|
2175
|
+
"name": "isLoading",
|
|
2176
|
+
"type": "boolean",
|
|
2177
|
+
"required": false,
|
|
2178
|
+
"default": "false",
|
|
2179
|
+
"description": "Loading state.",
|
|
2180
|
+
"category": "state"
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
"name": "error",
|
|
2184
|
+
"type": "string | null",
|
|
2185
|
+
"required": false,
|
|
2186
|
+
"default": "null",
|
|
2187
|
+
"description": "Error message.",
|
|
2188
|
+
"category": "state"
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
"name": "isEmpty",
|
|
2192
|
+
"type": "boolean",
|
|
2193
|
+
"required": false,
|
|
2194
|
+
"default": "false",
|
|
2195
|
+
"description": "Empty state.",
|
|
2196
|
+
"category": "state"
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
"name": "onRefresh",
|
|
2200
|
+
"type": "() => void",
|
|
2201
|
+
"required": true,
|
|
2202
|
+
"description": "Refresh callback.",
|
|
2203
|
+
"category": "callback"
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
"name": "onFollow",
|
|
2207
|
+
"type": "(userId: string) => void",
|
|
2208
|
+
"required": true,
|
|
2209
|
+
"description": "Follow callback.",
|
|
2210
|
+
"category": "callback"
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
"name": "onRetry",
|
|
2214
|
+
"type": "() => void",
|
|
2215
|
+
"required": false,
|
|
2216
|
+
"description": "Retry callback.",
|
|
2217
|
+
"category": "callback"
|
|
2218
|
+
}
|
|
2219
|
+
],
|
|
2220
|
+
"storyVariants": [
|
|
2221
|
+
{
|
|
2222
|
+
"name": "Default",
|
|
2223
|
+
"description": "Ready state.",
|
|
2224
|
+
"args": {
|
|
2225
|
+
"isLoading": false,
|
|
2226
|
+
"error": null,
|
|
2227
|
+
"isEmpty": false,
|
|
2228
|
+
"onRefresh": "fn()",
|
|
2229
|
+
"onFollow": "fn()",
|
|
2230
|
+
"onRetry": "fn()"
|
|
2231
|
+
},
|
|
2232
|
+
"needsPlayFunction": false
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
"name": "Loading",
|
|
2236
|
+
"description": "Loading state.",
|
|
2237
|
+
"args": {
|
|
2238
|
+
"isLoading": true,
|
|
2239
|
+
"error": null,
|
|
2240
|
+
"isEmpty": false,
|
|
2241
|
+
"onRefresh": "fn()",
|
|
2242
|
+
"onFollow": "fn()",
|
|
2243
|
+
"onRetry": "fn()"
|
|
2244
|
+
},
|
|
2245
|
+
"needsPlayFunction": false
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
"name": "Error",
|
|
2249
|
+
"description": "Error state.",
|
|
2250
|
+
"args": {
|
|
2251
|
+
"isLoading": false,
|
|
2252
|
+
"error": "Failed to load feed",
|
|
2253
|
+
"isEmpty": false,
|
|
2254
|
+
"onRefresh": "fn()",
|
|
2255
|
+
"onFollow": "fn()",
|
|
2256
|
+
"onRetry": "fn()"
|
|
2257
|
+
},
|
|
2258
|
+
"needsPlayFunction": true,
|
|
2259
|
+
"playDescription": "1. Click retry using userEvent.click(getByRole('button', { name: 'Retry' })), 2. Assert onRetry called"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
"name": "Empty",
|
|
2263
|
+
"description": "Empty state.",
|
|
2264
|
+
"args": {
|
|
2265
|
+
"isLoading": false,
|
|
2266
|
+
"error": null,
|
|
2267
|
+
"isEmpty": true,
|
|
2268
|
+
"onRefresh": "fn()",
|
|
2269
|
+
"onFollow": "fn()",
|
|
2270
|
+
"onRetry": "fn()"
|
|
2271
|
+
},
|
|
2272
|
+
"needsPlayFunction": false
|
|
2273
|
+
}
|
|
2274
|
+
],
|
|
2275
|
+
"dataContract": {
|
|
2276
|
+
"source": "graphql-query",
|
|
2277
|
+
"operationName": "GetFeed",
|
|
2278
|
+
"fields": [
|
|
2279
|
+
"feedItems[].id",
|
|
2280
|
+
"feedItems[].user.name",
|
|
2281
|
+
"feedItems[].user.avatar",
|
|
2282
|
+
"feedItems[].photoUrl",
|
|
2283
|
+
"feedItems[].caption",
|
|
2284
|
+
"feedItems[].likeCount",
|
|
2285
|
+
"feedItems[].commentCount",
|
|
2286
|
+
"suggestedUsers[].id",
|
|
2287
|
+
"suggestedUsers[].name",
|
|
2288
|
+
"suggestedUsers[].avatar"
|
|
2289
|
+
]
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
]
|