@auto-engineer/component-implementor-react 1.98.0 → 1.99.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 +74 -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,1432 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"componentId": "credit-card-input",
|
|
4
|
+
"componentName": "CreditCardInput",
|
|
5
|
+
"isNew": true,
|
|
6
|
+
"atomicType": "molecule",
|
|
7
|
+
"composes": ["ui-components-input", "ui-components-inputotp"],
|
|
8
|
+
"specDeltas": {
|
|
9
|
+
"structure": ["Composes inputs for card number (using InputOTP), expiration, CVV, name."],
|
|
10
|
+
"rendering": ["Shows validation errors inline for each field."],
|
|
11
|
+
"interaction": ["Validates card number format on change; calls onChange with full details."],
|
|
12
|
+
"styling": [
|
|
13
|
+
"Card number uses InputOTP with `space-x-2` for slots, each slot `h-10 w-10 rounded-md border border-input bg-background text-center text-sm focus:border-accent focus:outline-none`.",
|
|
14
|
+
"Expiry and CVV inputs use `h-10 rounded-md border border-input bg-background px-3 py-2 text-sm` with `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
15
|
+
"Name input uses `h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm`.",
|
|
16
|
+
"Invalid fields use `border-destructive focus:border-destructive`.",
|
|
17
|
+
"Error messages use `text-sm text-destructive mt-1`.",
|
|
18
|
+
"Labels use `text-sm font-medium leading-none`.",
|
|
19
|
+
"Group layout uses `grid grid-cols-2 gap-4` for expiry and CVV.",
|
|
20
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
21
|
+
"Touch targets use `min-h-11` with `touch-action: manipulation`."
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"props": [
|
|
25
|
+
{
|
|
26
|
+
"name": "value",
|
|
27
|
+
"type": "{ number: string; expiry: string; cvv: string; name: string }",
|
|
28
|
+
"required": false,
|
|
29
|
+
"description": "Current card details.",
|
|
30
|
+
"category": "state"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "onChange",
|
|
34
|
+
"type": "(details: { number: string; expiry: string; cvv: string; name: string }) => void",
|
|
35
|
+
"required": true,
|
|
36
|
+
"description": "Callback for changes.",
|
|
37
|
+
"category": "callback"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"storyVariants": [
|
|
41
|
+
{
|
|
42
|
+
"name": "Default",
|
|
43
|
+
"description": "Credit card inputs empty.",
|
|
44
|
+
"args": {
|
|
45
|
+
"onChange": "fn()"
|
|
46
|
+
},
|
|
47
|
+
"needsPlayFunction": true,
|
|
48
|
+
"playDescription": "Type card number with userEvent.type(getByLabelText('Card Number'), '4111111111111111');"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "Invalid",
|
|
52
|
+
"description": "Showing validation errors.",
|
|
53
|
+
"args": {
|
|
54
|
+
"onChange": "fn()"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"dataContract": {
|
|
59
|
+
"source": "local-state",
|
|
60
|
+
"fields": ["number", "expiry", "cvv", "name"]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"componentId": "listing-map",
|
|
65
|
+
"componentName": "ListingMap",
|
|
66
|
+
"isNew": true,
|
|
67
|
+
"atomicType": "organism",
|
|
68
|
+
"composes": ["ui-components-card"],
|
|
69
|
+
"specDeltas": {
|
|
70
|
+
"structure": ["Renders an interactive map with markers for listing locations, composing Card for container."],
|
|
71
|
+
"rendering": [
|
|
72
|
+
"In ready state, displays map with markers based on listings data; shows skeleton in loading state.",
|
|
73
|
+
"In no-results state, shows empty map with message overlay."
|
|
74
|
+
],
|
|
75
|
+
"interaction": ["Clicking a marker calls onMarkerClick with listing ID, navigating to details."],
|
|
76
|
+
"styling": [
|
|
77
|
+
"Map container uses `h-[500px] w-full rounded-lg border border-border shadow-sm`.",
|
|
78
|
+
"Markers use `bg-primary rounded-full h-4 w-4` with hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:scale-125 }` and `transition: transform 150ms ease`.",
|
|
79
|
+
"Skeleton uses `h-[500px] w-full bg-muted animate-pulse` with shimmer `background: linear-gradient(90deg, hsl(var(--muted)) 0%, hsl(var(--muted)/0.5) 50%, hsl(var(--muted)) 100%); background-size: 200% 100%; animation: shimmer 1.5s linear infinite`.",
|
|
80
|
+
"Empty message overlay uses `absolute inset-0 flex items-center justify-center bg-background/80 text-muted-foreground text-lg` with react-spring `useTransition(isEmpty, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: { tension: 400, friction: 26 } })`; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
81
|
+
"Marker click uses CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`.",
|
|
82
|
+
"Focus rings on interactive markers use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
83
|
+
"Touch targets for markers use `min-h-11 min-w-11` with `touch-action: manipulation`.",
|
|
84
|
+
"Map zoom transition uses react-spring `useSpring({ scale: zoomLevel, config: { tension: 170, friction: 26 } })`; respects `useReducedMotion()`.",
|
|
85
|
+
"Info window uses `bg-background p-2 rounded-md shadow-md` with `z-[500]`.",
|
|
86
|
+
"Hover on info window wrapped in `@media (hover: hover) and (pointer: fine) { hover:shadow-lg }` with `transition: box-shadow 150ms ease`."
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"props": [
|
|
90
|
+
{
|
|
91
|
+
"name": "listings",
|
|
92
|
+
"type": "Array<{ id: string; lat: number; lng: number }>",
|
|
93
|
+
"required": true,
|
|
94
|
+
"description": "List of listings with coordinates.",
|
|
95
|
+
"category": "data"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "isLoading",
|
|
99
|
+
"type": "boolean",
|
|
100
|
+
"required": false,
|
|
101
|
+
"default": "false",
|
|
102
|
+
"description": "Shows skeleton when true.",
|
|
103
|
+
"category": "state"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "onMarkerClick",
|
|
107
|
+
"type": "(id: string) => void",
|
|
108
|
+
"required": true,
|
|
109
|
+
"description": "Callback when marker is clicked.",
|
|
110
|
+
"category": "callback"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"storyVariants": [
|
|
114
|
+
{
|
|
115
|
+
"name": "Default",
|
|
116
|
+
"description": "Map with sample markers.",
|
|
117
|
+
"args": {
|
|
118
|
+
"listings": [
|
|
119
|
+
{
|
|
120
|
+
"id": "1",
|
|
121
|
+
"lat": 40.7128,
|
|
122
|
+
"lng": -74.006
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"onMarkerClick": "fn()"
|
|
126
|
+
},
|
|
127
|
+
"needsPlayFunction": true,
|
|
128
|
+
"playDescription": "Simulate marker click with userEvent.click(getByRole('button', { name: 'Marker 1' }));"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "Loading",
|
|
132
|
+
"description": "Map in loading state.",
|
|
133
|
+
"args": {
|
|
134
|
+
"isLoading": true,
|
|
135
|
+
"onMarkerClick": "fn()"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Empty",
|
|
140
|
+
"description": "Empty map state.",
|
|
141
|
+
"args": {
|
|
142
|
+
"listings": [],
|
|
143
|
+
"onMarkerClick": "fn()"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"dataContract": {
|
|
148
|
+
"source": "props",
|
|
149
|
+
"propsFieldName": "listings",
|
|
150
|
+
"fields": ["id", "lat", "lng"]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"componentId": "price-breakdown",
|
|
155
|
+
"componentName": "PriceBreakdown",
|
|
156
|
+
"isNew": true,
|
|
157
|
+
"atomicType": "organism",
|
|
158
|
+
"composes": ["ui-components-table"],
|
|
159
|
+
"specDeltas": {
|
|
160
|
+
"structure": ["Renders a table-like breakdown of prices, composing Table for display."],
|
|
161
|
+
"rendering": [
|
|
162
|
+
"Displays rows for base price, fees, taxes, total using Intl.NumberFormat for currency; uses tabular-nums for alignment."
|
|
163
|
+
],
|
|
164
|
+
"interaction": [],
|
|
165
|
+
"styling": [
|
|
166
|
+
"Table uses `w-full caption-bottom text-sm`.",
|
|
167
|
+
"Row uses `border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted`.",
|
|
168
|
+
"Cell uses `p-4 align-middle font-variant-numeric: tabular-nums`.",
|
|
169
|
+
"Label cell uses `text-left font-medium`.",
|
|
170
|
+
"Amount cell uses `text-right font-semibold`.",
|
|
171
|
+
"Total row uses `font-bold bg-muted`.",
|
|
172
|
+
"Currency symbols use `text-muted-foreground`.",
|
|
173
|
+
"Hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-muted/50 }` with `transition: background-color 150ms ease`.",
|
|
174
|
+
"No borders use `box-shadow: 0 0 0 1px rgba(0,0,0,0.08)` for separators.",
|
|
175
|
+
"Text wrap for labels uses `text-balance`."
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"props": [
|
|
179
|
+
{
|
|
180
|
+
"name": "breakdown",
|
|
181
|
+
"type": "Array<{ label: string; amount: number }>",
|
|
182
|
+
"required": true,
|
|
183
|
+
"description": "Price items to display.",
|
|
184
|
+
"category": "data"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "currency",
|
|
188
|
+
"type": "string",
|
|
189
|
+
"required": false,
|
|
190
|
+
"default": "'USD'",
|
|
191
|
+
"description": "Currency code for formatting.",
|
|
192
|
+
"category": "config"
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"storyVariants": [
|
|
196
|
+
{
|
|
197
|
+
"name": "Default",
|
|
198
|
+
"description": "Price breakdown with sample data.",
|
|
199
|
+
"args": {
|
|
200
|
+
"breakdown": [
|
|
201
|
+
{
|
|
202
|
+
"label": "Base",
|
|
203
|
+
"amount": 100
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"label": "Tax",
|
|
207
|
+
"amount": 10
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
"dataContract": {
|
|
214
|
+
"source": "props",
|
|
215
|
+
"propsFieldName": "breakdown",
|
|
216
|
+
"fields": ["label", "amount"]
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"componentId": "listing-card",
|
|
221
|
+
"componentName": "ListingCard",
|
|
222
|
+
"isNew": true,
|
|
223
|
+
"atomicType": "organism",
|
|
224
|
+
"composes": ["ui-components-card", "ui-components-badge"],
|
|
225
|
+
"specDeltas": {
|
|
226
|
+
"structure": ["Composes Card with image, title, rating Badge, price."],
|
|
227
|
+
"rendering": ["Displays listing data; shows availability Badge."],
|
|
228
|
+
"interaction": ["Clicking card navigates to details, calling onClick with listing ID."],
|
|
229
|
+
"styling": [
|
|
230
|
+
"Card uses `rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden`.",
|
|
231
|
+
"Image uses `aspect-[16/10] w-full object-cover` with skeleton `h-40 w-full bg-muted animate-pulse`.",
|
|
232
|
+
"Title uses `text-lg font-semibold tracking-tight text-balance`.",
|
|
233
|
+
"Rating badge uses `variant=\"secondary\"` rendering as `bg-secondary text-secondary-foreground rounded-full px-2 py-0.5 text-xs font-semibold`.",
|
|
234
|
+
"Price uses `text-lg font-bold font-variant-numeric: tabular-nums`.",
|
|
235
|
+
"Availability badge uses `variant=\"default\"` for available (`bg-green-500 text-white`), `variant=\"destructive\"` for unavailable (`bg-destructive text-destructive-foreground`).",
|
|
236
|
+
"Hover effect wrapped in `@media (hover: hover) and (pointer: fine) { hover:shadow-md hover:scale-[1.02] }` with `transition: transform 150ms ease, box-shadow 150ms ease`.",
|
|
237
|
+
"Focus ring uses `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
238
|
+
"Touch target uses entire card with `touch-action: manipulation`.",
|
|
239
|
+
"Content padding uses `p-4`."
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
"props": [
|
|
243
|
+
{
|
|
244
|
+
"name": "listing",
|
|
245
|
+
"type": "{ id: string; photo: string; title: string; rating: number; price: number; available: boolean }",
|
|
246
|
+
"required": true,
|
|
247
|
+
"description": "Listing data to display.",
|
|
248
|
+
"category": "data"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "onClick",
|
|
252
|
+
"type": "(id: string) => void",
|
|
253
|
+
"required": true,
|
|
254
|
+
"description": "Callback for viewing details.",
|
|
255
|
+
"category": "callback"
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"storyVariants": [
|
|
259
|
+
{
|
|
260
|
+
"name": "Default",
|
|
261
|
+
"description": "Listing card with data.",
|
|
262
|
+
"args": {
|
|
263
|
+
"listing": {
|
|
264
|
+
"id": "1",
|
|
265
|
+
"photo": "img.jpg",
|
|
266
|
+
"title": "Cozy Apartment",
|
|
267
|
+
"rating": 4.5,
|
|
268
|
+
"price": 100,
|
|
269
|
+
"available": true
|
|
270
|
+
},
|
|
271
|
+
"onClick": "fn()"
|
|
272
|
+
},
|
|
273
|
+
"needsPlayFunction": true,
|
|
274
|
+
"playDescription": "Click the card with userEvent.click(getByRole('article'));"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "Unavailable",
|
|
278
|
+
"description": "Card showing unavailable.",
|
|
279
|
+
"args": {
|
|
280
|
+
"listing": {
|
|
281
|
+
"id": "1",
|
|
282
|
+
"photo": "img.jpg",
|
|
283
|
+
"title": "Cozy Apartment",
|
|
284
|
+
"rating": 4.5,
|
|
285
|
+
"price": 100,
|
|
286
|
+
"available": false
|
|
287
|
+
},
|
|
288
|
+
"onClick": "fn()"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
"dataContract": {
|
|
293
|
+
"source": "props",
|
|
294
|
+
"propsFieldName": "listing",
|
|
295
|
+
"fields": ["id", "photo", "title", "rating", "price", "available"]
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"componentId": "ui-components-combobox",
|
|
300
|
+
"componentName": "Combobox",
|
|
301
|
+
"isNew": false,
|
|
302
|
+
"specDeltas": {
|
|
303
|
+
"structure": [
|
|
304
|
+
"Renders as a text input with a dropdown list of filtered options, using Button for trigger and InputGroup for composition.",
|
|
305
|
+
"Supports autocomplete for location search by filtering options based on user input."
|
|
306
|
+
],
|
|
307
|
+
"rendering": [
|
|
308
|
+
"In ready state, displays placeholder text and dropdown trigger.",
|
|
309
|
+
"When focused or triggered, shows dropdown with filtered options; if no matches, shows emptyMessage.",
|
|
310
|
+
"Selected value displays in the input field."
|
|
311
|
+
],
|
|
312
|
+
"interaction": [
|
|
313
|
+
"Typing in the input filters options case-insensitively, trimming whitespace, and calls onValueChange with the selected value on selection.",
|
|
314
|
+
"Arrow keys navigate through visible options; Enter selects the highlighted option.",
|
|
315
|
+
"Escape closes the dropdown without selection."
|
|
316
|
+
],
|
|
317
|
+
"styling": [
|
|
318
|
+
"Combobox input uses `h-10 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`.",
|
|
319
|
+
"Dropdown content uses `mt-1 w-full rounded-md border border-border bg-popover p-1 shadow-md` with react-spring `useTransition(isOpen, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, leave: { opacity: 0, scale: 0.95 }, config: { tension: 400, friction: 22 } })`; respects `useReducedMotion()` by setting `immediate: true` when motion is reduced.",
|
|
320
|
+
"Option items use `flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50` with hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent hover:text-accent-foreground }` and `transition: background-color 150ms ease`.",
|
|
321
|
+
"Selected option uses `bg-accent text-accent-foreground`.",
|
|
322
|
+
"Trigger button uses `variant=\"ghost\"` rendering as `bg-transparent hover:bg-accent hover:text-accent-foreground` with `min-h-11 min-w-11 touch-action: manipulation`.",
|
|
323
|
+
"Empty message uses `p-4 text-center text-sm text-muted-foreground`.",
|
|
324
|
+
"Clear button uses `variant=\"ghost\"` with CSS `active:scale-[0.97]` and `transition: transform 100ms ease-out`.",
|
|
325
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
326
|
+
"Touch targets for options use `min-h-11` with `touch-action: manipulation`."
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"props": [
|
|
330
|
+
{
|
|
331
|
+
"name": "options",
|
|
332
|
+
"type": "Array<{ value: string; label: string }>",
|
|
333
|
+
"required": true,
|
|
334
|
+
"description": "List of selectable options for autocomplete.",
|
|
335
|
+
"category": "data"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "value",
|
|
339
|
+
"type": "string",
|
|
340
|
+
"required": false,
|
|
341
|
+
"default": "''",
|
|
342
|
+
"description": "Currently selected value.",
|
|
343
|
+
"category": "state"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "onValueChange",
|
|
347
|
+
"type": "(value: string) => void",
|
|
348
|
+
"required": true,
|
|
349
|
+
"description": "Callback fired when a new value is selected.",
|
|
350
|
+
"category": "callback"
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"name": "placeholder",
|
|
354
|
+
"type": "string",
|
|
355
|
+
"required": false,
|
|
356
|
+
"default": "'Select...'",
|
|
357
|
+
"description": "Placeholder text when no value is selected.",
|
|
358
|
+
"category": "config"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"name": "emptyMessage",
|
|
362
|
+
"type": "string",
|
|
363
|
+
"required": false,
|
|
364
|
+
"default": "'No options found.'",
|
|
365
|
+
"description": "Message shown when no options match the filter.",
|
|
366
|
+
"category": "config"
|
|
367
|
+
}
|
|
368
|
+
],
|
|
369
|
+
"storyVariants": [
|
|
370
|
+
{
|
|
371
|
+
"name": "Default",
|
|
372
|
+
"description": "Combobox in ready state with options.",
|
|
373
|
+
"args": {
|
|
374
|
+
"options": [
|
|
375
|
+
{
|
|
376
|
+
"value": "nyc",
|
|
377
|
+
"label": "New York City"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"value": "la",
|
|
381
|
+
"label": "Los Angeles"
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
"placeholder": "Search location",
|
|
385
|
+
"onValueChange": "fn()"
|
|
386
|
+
},
|
|
387
|
+
"needsPlayFunction": true,
|
|
388
|
+
"playDescription": "1. Focus the input with userEvent.click(getByRole('combobox')); 2. Type 'n' with userEvent.type(getByRole('combobox'), 'n'); 3. Await waitFor(() => expect(getByText('New York City')).toBeVisible()); 4. Click the option with userEvent.click(getByText('New York City')); 5. Expect the input value to be 'nyc'."
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"name": "NoResults",
|
|
392
|
+
"description": "Combobox showing no results message.",
|
|
393
|
+
"args": {
|
|
394
|
+
"options": [
|
|
395
|
+
{
|
|
396
|
+
"value": "nyc",
|
|
397
|
+
"label": "New York City"
|
|
398
|
+
}
|
|
399
|
+
],
|
|
400
|
+
"placeholder": "Search location",
|
|
401
|
+
"emptyMessage": "No locations found.",
|
|
402
|
+
"onValueChange": "fn()"
|
|
403
|
+
},
|
|
404
|
+
"needsPlayFunction": true,
|
|
405
|
+
"playDescription": "1. Focus the input with userEvent.click(getByRole('combobox')); 2. Type 'xyz' with userEvent.type(getByRole('combobox'), 'xyz'); 3. Await waitFor(() => expect(getByText('No locations found.')).toBeVisible());"
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
"dataContract": {
|
|
409
|
+
"source": "props",
|
|
410
|
+
"propsFieldName": "options",
|
|
411
|
+
"fields": ["value", "label"]
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"componentId": "ui-components-calendar",
|
|
416
|
+
"componentName": "Calendar",
|
|
417
|
+
"isNew": false,
|
|
418
|
+
"specDeltas": {
|
|
419
|
+
"structure": [
|
|
420
|
+
"Renders a calendar grid with selectable dates, supporting range mode for check-in/check-out selection."
|
|
421
|
+
],
|
|
422
|
+
"rendering": [
|
|
423
|
+
"In ready state, displays current month with selectable dates; disables past dates and unavailable dates based on disabled prop.",
|
|
424
|
+
"Shows selected range with from and to dates highlighted; intermediate dates in range get accent styling.",
|
|
425
|
+
"Disables dates before today by default."
|
|
426
|
+
],
|
|
427
|
+
"interaction": [
|
|
428
|
+
"Clicking a date selects it in single mode or extends the range in range mode; calls onSelect with the selected date or range.",
|
|
429
|
+
"Validates that no unavailable dates exist within the selected range, checking every day using eachDayOfInterval.",
|
|
430
|
+
"Arrow keys navigate between days; Enter selects the focused day."
|
|
431
|
+
],
|
|
432
|
+
"styling": [
|
|
433
|
+
"Calendar grid uses `w-full border-collapse space-y-1`.",
|
|
434
|
+
"Day cells use `h-9 w-9 text-center text-sm p-0 relative focus-within:relative focus-within:z-20` with hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }` and `transition: background-color 150ms ease`.",
|
|
435
|
+
"Selected day uses `bg-primary text-primary-foreground` with react-spring `useTransition(selected, { opacity: 0 to 1, config: { tension: 300, friction: 22 } })`; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
436
|
+
"Disabled days use `opacity-50 cursor-not-allowed`.",
|
|
437
|
+
"Range in-between days use `bg-accent text-accent-foreground`.",
|
|
438
|
+
"Navigation buttons use `variant=\"ghost\"` rendering as `bg-transparent hover:bg-accent hover:text-accent-foreground` with `min-h-11 min-w-11 touch-action: manipulation` and CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`.",
|
|
439
|
+
"Today indicator uses `ring-1 ring-accent`.",
|
|
440
|
+
"Outside days use `text-muted-foreground`.",
|
|
441
|
+
"Focus rings on days use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
442
|
+
"Numbers in cells use `font-variant-numeric: tabular-nums`.",
|
|
443
|
+
"Month transition uses react-spring `useTransition(month, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: { tension: 400, friction: 26 } })`; respects `useReducedMotion()` by setting `immediate: true`."
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
"props": [
|
|
447
|
+
{
|
|
448
|
+
"name": "mode",
|
|
449
|
+
"type": "'single' | 'multiple' | 'range'",
|
|
450
|
+
"required": true,
|
|
451
|
+
"description": "Selection mode for dates.",
|
|
452
|
+
"category": "config"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"name": "selected",
|
|
456
|
+
"type": "Date | Date[] | { from: Date; to?: Date }",
|
|
457
|
+
"required": false,
|
|
458
|
+
"description": "Currently selected date(s) or range.",
|
|
459
|
+
"category": "state"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"name": "onSelect",
|
|
463
|
+
"type": "(date: Date | Date[] | { from: Date; to: Date }) => void",
|
|
464
|
+
"required": true,
|
|
465
|
+
"description": "Callback fired when a date is selected.",
|
|
466
|
+
"category": "callback"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "disabled",
|
|
470
|
+
"type": "Date[] | ((date: Date) => boolean)",
|
|
471
|
+
"required": false,
|
|
472
|
+
"description": "Dates or matcher to disable (e.g., past or unavailable).",
|
|
473
|
+
"category": "config"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"name": "size",
|
|
477
|
+
"type": "\"sm\" | \"md\" | \"lg\"",
|
|
478
|
+
"required": false,
|
|
479
|
+
"default": "\"md\"",
|
|
480
|
+
"description": "Controls calendar size variants.",
|
|
481
|
+
"category": "visual"
|
|
482
|
+
}
|
|
483
|
+
],
|
|
484
|
+
"storyVariants": [
|
|
485
|
+
{
|
|
486
|
+
"name": "Default",
|
|
487
|
+
"description": "Calendar in range mode with some disabled dates.",
|
|
488
|
+
"args": {
|
|
489
|
+
"mode": "range",
|
|
490
|
+
"disabled": "[new Date('2023-01-01'), new Date('2023-01-02')]",
|
|
491
|
+
"onSelect": "fn()",
|
|
492
|
+
"size": "md"
|
|
493
|
+
},
|
|
494
|
+
"needsPlayFunction": true,
|
|
495
|
+
"playDescription": "1. Click a start date with userEvent.click(getByText('15')); 2. Click an end date with userEvent.click(getByText('20')); 3. Await waitFor(() => expect(getByText('15')).toHaveClass('selected') && expect(getByText('20')).toHaveClass('selected'));"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"name": "DisabledDates",
|
|
499
|
+
"description": "Calendar showing disabled past dates.",
|
|
500
|
+
"args": {
|
|
501
|
+
"mode": "range",
|
|
502
|
+
"disabled": "((date) => date < new Date())",
|
|
503
|
+
"onSelect": "fn()",
|
|
504
|
+
"size": "md"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
],
|
|
508
|
+
"dataContract": {
|
|
509
|
+
"source": "props",
|
|
510
|
+
"propsFieldName": "disabled",
|
|
511
|
+
"fields": ["date"]
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"componentId": "ui-components-select",
|
|
516
|
+
"componentName": "Select",
|
|
517
|
+
"isNew": false,
|
|
518
|
+
"specDeltas": {
|
|
519
|
+
"structure": ["Renders a dropdown select for guest count or similar selections."],
|
|
520
|
+
"rendering": ["In ready state, shows placeholder or selected value; dropdown lists options."],
|
|
521
|
+
"interaction": ["Selecting an option calls onValueChange with the selected value."],
|
|
522
|
+
"styling": [
|
|
523
|
+
"Select trigger uses `flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50` with `min-h-11 touch-action: manipulation`.",
|
|
524
|
+
"Select content uses `w-full rounded-md border bg-popover p-1 text-popover-foreground shadow-md` with react-spring `useTransition(isOpen, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, leave: { opacity: 0, scale: 0.95 }, config: { tension: 400, friction: 22 } })`; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
525
|
+
"Select item uses `relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50` with hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent hover:text-accent-foreground }` and `transition: background-color 150ms ease`.",
|
|
526
|
+
"Selected item indicator uses `absolute right-2 flex h-3.5 w-3.5 items-center justify-center text-primary`.",
|
|
527
|
+
"Placeholder uses `text-muted-foreground`.",
|
|
528
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`."
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
"props": [
|
|
532
|
+
{
|
|
533
|
+
"name": "value",
|
|
534
|
+
"type": "string",
|
|
535
|
+
"required": false,
|
|
536
|
+
"description": "Currently selected value.",
|
|
537
|
+
"category": "state"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"name": "onValueChange",
|
|
541
|
+
"type": "(value: string) => void",
|
|
542
|
+
"required": true,
|
|
543
|
+
"description": "Callback fired when value changes.",
|
|
544
|
+
"category": "callback"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"name": "placeholder",
|
|
548
|
+
"type": "string",
|
|
549
|
+
"required": false,
|
|
550
|
+
"default": "'Select...'",
|
|
551
|
+
"description": "Placeholder text.",
|
|
552
|
+
"category": "config"
|
|
553
|
+
}
|
|
554
|
+
],
|
|
555
|
+
"storyVariants": [
|
|
556
|
+
{
|
|
557
|
+
"name": "Default",
|
|
558
|
+
"description": "Select with guest count options.",
|
|
559
|
+
"args": {
|
|
560
|
+
"placeholder": "Guests",
|
|
561
|
+
"onValueChange": "fn()"
|
|
562
|
+
},
|
|
563
|
+
"needsPlayFunction": true,
|
|
564
|
+
"playDescription": "1. Click the trigger with userEvent.click(getByRole('button')); 2. Click an option with userEvent.click(getByText('2 guests')); 3. Expect the value to be '2'."
|
|
565
|
+
}
|
|
566
|
+
],
|
|
567
|
+
"dataContract": {
|
|
568
|
+
"source": "props",
|
|
569
|
+
"propsFieldName": "options",
|
|
570
|
+
"fields": ["value", "label"]
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"componentId": "ui-components-form",
|
|
575
|
+
"componentName": "Form",
|
|
576
|
+
"isNew": false,
|
|
577
|
+
"specDeltas": {
|
|
578
|
+
"structure": ["Wraps form fields for booking and payment inputs, using react-hook-form integration."],
|
|
579
|
+
"rendering": [
|
|
580
|
+
"In editing-invalid state, shows FormMessage with error text near invalid fields; hides submit button or disables it.",
|
|
581
|
+
"In ready state, shows all fields without errors."
|
|
582
|
+
],
|
|
583
|
+
"interaction": [
|
|
584
|
+
"On submit, validates fields and calls onSubmit if valid; transitions to submitting state.",
|
|
585
|
+
"Real-time validation on blur for each field, showing aria-invalid and error message."
|
|
586
|
+
],
|
|
587
|
+
"styling": [
|
|
588
|
+
"FormItem uses `flex flex-col space-y-1.5`.",
|
|
589
|
+
"FormLabel uses `text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70` with `text-destructive` when invalid.",
|
|
590
|
+
"FormControl uses `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `border-destructive` when invalid.",
|
|
591
|
+
"FormMessage uses `text-sm text-destructive` for errors, visible only when invalid.",
|
|
592
|
+
"FormDescription uses `text-sm text-muted-foreground`.",
|
|
593
|
+
"Submit button uses `variant=\"default\"` rendering as `bg-primary text-primary-foreground hover:bg-primary/90` with `min-h-11 touch-action: manipulation` and CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`.",
|
|
594
|
+
"Disabled state uses `opacity-50 cursor-not-allowed`.",
|
|
595
|
+
"Hover on labels wrapped in `@media (hover: hover) and (pointer: fine) { hover:text-accent-foreground }` with `transition: color 150ms ease`."
|
|
596
|
+
]
|
|
597
|
+
},
|
|
598
|
+
"props": [
|
|
599
|
+
{
|
|
600
|
+
"name": "form",
|
|
601
|
+
"type": "UseFormReturn",
|
|
602
|
+
"required": true,
|
|
603
|
+
"description": "react-hook-form instance.",
|
|
604
|
+
"category": "config"
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"name": "onSubmit",
|
|
608
|
+
"type": "(data: FormData) => void",
|
|
609
|
+
"required": true,
|
|
610
|
+
"description": "Callback on valid form submission.",
|
|
611
|
+
"category": "callback"
|
|
612
|
+
}
|
|
613
|
+
],
|
|
614
|
+
"storyVariants": [
|
|
615
|
+
{
|
|
616
|
+
"name": "Default",
|
|
617
|
+
"description": "Form in ready state.",
|
|
618
|
+
"args": {
|
|
619
|
+
"onSubmit": "fn()"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"name": "Invalid",
|
|
624
|
+
"description": "Form showing validation errors.",
|
|
625
|
+
"args": {
|
|
626
|
+
"onSubmit": "fn()"
|
|
627
|
+
},
|
|
628
|
+
"needsPlayFunction": true,
|
|
629
|
+
"playDescription": "1. Type invalid data in input with userEvent.type(getByLabelText('Card Number'), 'invalid'); 2. Click submit with userEvent.click(getByRole('button', { name: 'Pay' })); 3. Await waitFor(() => expect(getByText('Invalid card number')).toBeVisible());"
|
|
630
|
+
}
|
|
631
|
+
],
|
|
632
|
+
"dataContract": {
|
|
633
|
+
"source": "local-state",
|
|
634
|
+
"fields": ["fieldName", "value", "error"]
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
"componentId": "ui-components-alert",
|
|
639
|
+
"componentName": "Alert",
|
|
640
|
+
"isNew": false,
|
|
641
|
+
"specDeltas": {
|
|
642
|
+
"structure": ["Renders as a banner for feedback like no-results or errors."],
|
|
643
|
+
"rendering": [
|
|
644
|
+
"In no-results state, shows info variant with message 'No listings match your criteria.'; hides results-list content.",
|
|
645
|
+
"In error state, shows destructive variant with message and retry button; hides other regions."
|
|
646
|
+
],
|
|
647
|
+
"interaction": ["Clicking retry in error banner calls onRetry, transitioning to loading state."],
|
|
648
|
+
"styling": [
|
|
649
|
+
"Alert root uses `relative w-full rounded-lg border p-4` with `border-border text-foreground` for default, `border-destructive text-destructive` for destructive.",
|
|
650
|
+
"AlertTitle uses `mb-1 font-medium`.",
|
|
651
|
+
"AlertDescription uses `text-sm`.",
|
|
652
|
+
"Retry button in destructive variant uses `variant=\"destructive\"` rendering as `bg-destructive text-destructive-foreground hover:bg-destructive/90` with `min-h-11 touch-action: manipulation` and CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`.",
|
|
653
|
+
"Icon uses `h-4 w-4 text-muted-foreground`.",
|
|
654
|
+
"Hover on retry button wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-destructive/90 }` with `transition: background-color 150ms ease`."
|
|
655
|
+
]
|
|
656
|
+
},
|
|
657
|
+
"props": [
|
|
658
|
+
{
|
|
659
|
+
"name": "variant",
|
|
660
|
+
"type": "'default' | 'destructive'",
|
|
661
|
+
"required": false,
|
|
662
|
+
"default": "'default'",
|
|
663
|
+
"description": "Visual variant for severity.",
|
|
664
|
+
"category": "config"
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
"name": "onRetry",
|
|
668
|
+
"type": "() => void",
|
|
669
|
+
"required": false,
|
|
670
|
+
"description": "Callback for retry action in error state.",
|
|
671
|
+
"category": "callback"
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
"storyVariants": [
|
|
675
|
+
{
|
|
676
|
+
"name": "Default",
|
|
677
|
+
"description": "Info alert for no results.",
|
|
678
|
+
"args": {
|
|
679
|
+
"variant": "default"
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
"name": "Error",
|
|
684
|
+
"description": "Destructive alert with retry.",
|
|
685
|
+
"args": {
|
|
686
|
+
"variant": "destructive",
|
|
687
|
+
"onRetry": "fn()"
|
|
688
|
+
},
|
|
689
|
+
"needsPlayFunction": true,
|
|
690
|
+
"playDescription": "Click the retry button with userEvent.click(getByRole('button', { name: 'Retry' }));"
|
|
691
|
+
}
|
|
692
|
+
],
|
|
693
|
+
"dataContract": {
|
|
694
|
+
"source": "props",
|
|
695
|
+
"propsFieldName": "message",
|
|
696
|
+
"fields": ["text"]
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"componentId": "ui-components-carousel",
|
|
701
|
+
"componentName": "Carousel",
|
|
702
|
+
"isNew": false,
|
|
703
|
+
"specDeltas": {
|
|
704
|
+
"structure": ["Renders listing photos in a slideshow."],
|
|
705
|
+
"rendering": ["Displays images in CarouselItem slides."],
|
|
706
|
+
"interaction": ["Swipe or arrow buttons navigate slides."],
|
|
707
|
+
"styling": [
|
|
708
|
+
"Carousel content uses `relative overflow-hidden` with react-spring `useTransition(activeIndex, { from: { transform: 'translateX(100%)' }, enter: { transform: 'translateX(0%)' }, leave: { transform: 'translateX(-100%)' }, config: { tension: 170, friction: 26 } })` for horizontal slides; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
709
|
+
"Carousel item uses `flex-shrink-0 w-full` with `aspect-ratio` for image containers to prevent layout shift.",
|
|
710
|
+
"Previous/Next buttons use `variant=\"outline\"` rendering as `border border-input bg-background hover:bg-accent hover:text-accent-foreground` with `min-h-11 min-w-11 touch-action: manipulation` positioned at edges with `z-[500]`.",
|
|
711
|
+
"Image uses `w-full h-auto object-cover` with skeleton `h-64 w-full bg-muted animate-pulse` for loading.",
|
|
712
|
+
"Hover on buttons wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }` with `transition: background-color 150ms ease`.",
|
|
713
|
+
"Focus rings on buttons use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
714
|
+
"Disabled buttons use `opacity-50 cursor-not-allowed`."
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
"props": [
|
|
718
|
+
{
|
|
719
|
+
"name": "images",
|
|
720
|
+
"type": "Array<{ src: string; alt: string }>",
|
|
721
|
+
"required": true,
|
|
722
|
+
"description": "List of images to display.",
|
|
723
|
+
"category": "data"
|
|
724
|
+
}
|
|
725
|
+
],
|
|
726
|
+
"storyVariants": [
|
|
727
|
+
{
|
|
728
|
+
"name": "Default",
|
|
729
|
+
"description": "Carousel with sample images.",
|
|
730
|
+
"args": {
|
|
731
|
+
"images": [
|
|
732
|
+
{
|
|
733
|
+
"src": "img1.jpg",
|
|
734
|
+
"alt": "Listing photo 1"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
"src": "img2.jpg",
|
|
738
|
+
"alt": "Listing photo 2"
|
|
739
|
+
}
|
|
740
|
+
]
|
|
741
|
+
},
|
|
742
|
+
"needsPlayFunction": true,
|
|
743
|
+
"playDescription": "Click next button with userEvent.click(getByRole('button', { name: 'Next' })); await waitFor(() => expect(getByAltText('Listing photo 2')).toBeVisible());"
|
|
744
|
+
}
|
|
745
|
+
],
|
|
746
|
+
"dataContract": {
|
|
747
|
+
"source": "props",
|
|
748
|
+
"propsFieldName": "images",
|
|
749
|
+
"fields": ["src", "alt"]
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"componentId": "ui-components-alertdialog",
|
|
754
|
+
"componentName": "AlertDialog",
|
|
755
|
+
"isNew": false,
|
|
756
|
+
"specDeltas": {
|
|
757
|
+
"structure": ["Used for unsaved changes confirmation before navigating away."],
|
|
758
|
+
"rendering": ["Shows when attempting to leave with unsaved changes, displaying warning message."],
|
|
759
|
+
"interaction": ["Confirm button proceeds with navigation; cancel closes dialog."],
|
|
760
|
+
"styling": [
|
|
761
|
+
"AlertDialog content uses `z-[400] fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-md bg-background p-6 shadow-lg` with react-spring `useTransition(isOpen, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, leave: { opacity: 0, scale: 0.95 }, config: { tension: 300, friction: 22 } })`; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
762
|
+
"Overlay uses `z-[300] fixed inset-0 bg-black/80` with same react-spring config as content for paired animation.",
|
|
763
|
+
"AlertDialogTitle uses `text-lg font-semibold`.",
|
|
764
|
+
"AlertDialogDescription uses `mt-2 mb-4 text-sm text-muted-foreground`.",
|
|
765
|
+
"Confirm button uses `variant=\"default\"` rendering as `bg-primary text-primary-foreground hover:bg-primary/90` with `min-h-11 touch-action: manipulation` and CSS `active:scale-[0.97]` with `transition: transform 100ms ease-out`.",
|
|
766
|
+
"Cancel button uses `variant=\"ghost\"` rendering as `bg-transparent hover:bg-accent hover:text-accent-foreground`.",
|
|
767
|
+
"Hover on buttons wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }` for confirm, with `transition: background-color 150ms ease`."
|
|
768
|
+
]
|
|
769
|
+
},
|
|
770
|
+
"props": [
|
|
771
|
+
{
|
|
772
|
+
"name": "open",
|
|
773
|
+
"type": "boolean",
|
|
774
|
+
"required": true,
|
|
775
|
+
"description": "Controls dialog visibility.",
|
|
776
|
+
"category": "state"
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
"name": "onOpenChange",
|
|
780
|
+
"type": "(open: boolean) => void",
|
|
781
|
+
"required": true,
|
|
782
|
+
"description": "Callback for open state change.",
|
|
783
|
+
"category": "callback"
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
"name": "onConfirm",
|
|
787
|
+
"type": "() => void",
|
|
788
|
+
"required": true,
|
|
789
|
+
"description": "Callback to proceed with action.",
|
|
790
|
+
"category": "callback"
|
|
791
|
+
}
|
|
792
|
+
],
|
|
793
|
+
"storyVariants": [
|
|
794
|
+
{
|
|
795
|
+
"name": "Default",
|
|
796
|
+
"description": "Confirmation dialog open.",
|
|
797
|
+
"args": {
|
|
798
|
+
"open": true,
|
|
799
|
+
"onOpenChange": "fn()",
|
|
800
|
+
"onConfirm": "fn()"
|
|
801
|
+
},
|
|
802
|
+
"needsPlayFunction": true,
|
|
803
|
+
"playDescription": "Click confirm with userEvent.click(getByRole('button', { name: 'Confirm' }));"
|
|
804
|
+
}
|
|
805
|
+
],
|
|
806
|
+
"dataContract": {
|
|
807
|
+
"source": "local-state",
|
|
808
|
+
"fields": ["message"]
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
"componentId": "ui-components-card",
|
|
813
|
+
"componentName": "Card",
|
|
814
|
+
"isNew": false,
|
|
815
|
+
"specDeltas": {
|
|
816
|
+
"structure": ["Used as base for listing previews in results grid."],
|
|
817
|
+
"rendering": ["Displays photo, title, price in CardContent."],
|
|
818
|
+
"interaction": [],
|
|
819
|
+
"styling": [
|
|
820
|
+
"Card root uses `rounded-lg border bg-card text-card-foreground shadow-sm`.",
|
|
821
|
+
"CardHeader uses `flex flex-col space-y-1.5 p-6`.",
|
|
822
|
+
"CardTitle uses `text-2xl font-semibold leading-none tracking-tight text-balance`.",
|
|
823
|
+
"CardDescription uses `text-sm text-muted-foreground`.",
|
|
824
|
+
"CardContent uses `p-6 pt-0`.",
|
|
825
|
+
"Hover effect wrapped in `@media (hover: hover) and (pointer: fine) { hover:shadow-md }` with `transition: box-shadow 150ms ease`."
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
"props": [
|
|
829
|
+
{
|
|
830
|
+
"name": "onClick",
|
|
831
|
+
"type": "() => void",
|
|
832
|
+
"required": false,
|
|
833
|
+
"description": "Callback for card click to view details.",
|
|
834
|
+
"category": "callback"
|
|
835
|
+
}
|
|
836
|
+
],
|
|
837
|
+
"storyVariants": [
|
|
838
|
+
{
|
|
839
|
+
"name": "Default",
|
|
840
|
+
"description": "Card with sample listing data.",
|
|
841
|
+
"args": {
|
|
842
|
+
"onClick": "fn()"
|
|
843
|
+
},
|
|
844
|
+
"needsPlayFunction": true,
|
|
845
|
+
"playDescription": "Click the card with userEvent.click(getByRole('article'));"
|
|
846
|
+
}
|
|
847
|
+
],
|
|
848
|
+
"dataContract": {
|
|
849
|
+
"source": "props",
|
|
850
|
+
"propsFieldName": "listing",
|
|
851
|
+
"fields": ["photo", "title", "price"]
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
"componentId": "ui-components-pagination",
|
|
856
|
+
"componentName": "Pagination",
|
|
857
|
+
"isNew": false,
|
|
858
|
+
"specDeltas": {
|
|
859
|
+
"structure": ["Renders pagination controls for results list."],
|
|
860
|
+
"rendering": ["Shows page links, previous/next based on current page and total."],
|
|
861
|
+
"interaction": ["Clicking page link calls onPageChange with new page."],
|
|
862
|
+
"styling": [
|
|
863
|
+
"Pagination content uses `flex items-center gap-1`.",
|
|
864
|
+
"Pagination item uses `relative inline-flex items-center`.",
|
|
865
|
+
"Pagination link uses `variant=\"ghost\"` rendering as `bg-transparent hover:bg-accent hover:text-accent-foreground h-10 px-4` with `min-h-11 min-w-11 touch-action: manipulation` and `transition: background-color 150ms ease`.",
|
|
866
|
+
"Active page uses `border border-input bg-background`.",
|
|
867
|
+
"Previous/Next use `variant=\"ghost\"` with icons, disabled `opacity-50 cursor-not-allowed`.",
|
|
868
|
+
"Ellipsis uses `h-10 w-10 flex items-center justify-center text-muted-foreground`.",
|
|
869
|
+
"Hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-accent }`.",
|
|
870
|
+
"Focus rings use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`."
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
"props": [
|
|
874
|
+
{
|
|
875
|
+
"name": "currentPage",
|
|
876
|
+
"type": "number",
|
|
877
|
+
"required": true,
|
|
878
|
+
"description": "Current active page.",
|
|
879
|
+
"category": "state"
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"name": "totalPages",
|
|
883
|
+
"type": "number",
|
|
884
|
+
"required": true,
|
|
885
|
+
"description": "Total number of pages.",
|
|
886
|
+
"category": "data"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
"name": "onPageChange",
|
|
890
|
+
"type": "(page: number) => void",
|
|
891
|
+
"required": true,
|
|
892
|
+
"description": "Callback for page change.",
|
|
893
|
+
"category": "callback"
|
|
894
|
+
}
|
|
895
|
+
],
|
|
896
|
+
"storyVariants": [
|
|
897
|
+
{
|
|
898
|
+
"name": "Default",
|
|
899
|
+
"description": "Pagination with 5 pages, current 2.",
|
|
900
|
+
"args": {
|
|
901
|
+
"currentPage": 2,
|
|
902
|
+
"totalPages": 5,
|
|
903
|
+
"onPageChange": "fn()"
|
|
904
|
+
},
|
|
905
|
+
"needsPlayFunction": true,
|
|
906
|
+
"playDescription": "Click next with userEvent.click(getByRole('button', { name: 'Next' }));"
|
|
907
|
+
}
|
|
908
|
+
],
|
|
909
|
+
"dataContract": {
|
|
910
|
+
"source": "props",
|
|
911
|
+
"propsFieldName": "totalPages",
|
|
912
|
+
"fields": ["pageNumber"]
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
"componentId": "ui-components-spinner",
|
|
917
|
+
"componentName": "Spinner",
|
|
918
|
+
"isNew": false,
|
|
919
|
+
"specDeltas": {
|
|
920
|
+
"structure": [],
|
|
921
|
+
"rendering": ["Shows in loading and submitting states as progress indicator."],
|
|
922
|
+
"interaction": [],
|
|
923
|
+
"styling": [
|
|
924
|
+
"Spinner uses `h-8 w-8 animate-spin text-muted-foreground`.",
|
|
925
|
+
"Container uses `flex items-center justify-center h-full`.",
|
|
926
|
+
"Respects prefers-reduced-motion with `@media (prefers-reduced-motion: reduce) { animation: none }`."
|
|
927
|
+
]
|
|
928
|
+
},
|
|
929
|
+
"props": [
|
|
930
|
+
{
|
|
931
|
+
"name": "isLoading",
|
|
932
|
+
"type": "boolean",
|
|
933
|
+
"required": false,
|
|
934
|
+
"default": "false",
|
|
935
|
+
"description": "Controls visibility for loading state.",
|
|
936
|
+
"category": "state"
|
|
937
|
+
}
|
|
938
|
+
],
|
|
939
|
+
"storyVariants": [
|
|
940
|
+
{
|
|
941
|
+
"name": "Default",
|
|
942
|
+
"description": "Spinner in loading state.",
|
|
943
|
+
"args": {
|
|
944
|
+
"isLoading": true
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
],
|
|
948
|
+
"dataContract": {
|
|
949
|
+
"source": "local-state",
|
|
950
|
+
"fields": []
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"componentId": "search-browse-listings-page",
|
|
955
|
+
"componentName": "SearchBrowseListingsPage",
|
|
956
|
+
"isNew": true,
|
|
957
|
+
"atomicType": "page",
|
|
958
|
+
"composes": [
|
|
959
|
+
"ui-components-combobox",
|
|
960
|
+
"ui-components-calendar",
|
|
961
|
+
"ui-components-select",
|
|
962
|
+
"ui-components-pagination",
|
|
963
|
+
"listing-map",
|
|
964
|
+
"listing-card",
|
|
965
|
+
"ui-components-spinner",
|
|
966
|
+
"ui-components-alert"
|
|
967
|
+
],
|
|
968
|
+
"specDeltas": {
|
|
969
|
+
"structure": [
|
|
970
|
+
"<main> with search-filters in header section, results-list in main content as grid of ListingCard, map-view in aside."
|
|
971
|
+
],
|
|
972
|
+
"rendering": [
|
|
973
|
+
"In loading state, shows Spinner in results-list and map-view regions; hides no-results feedback.",
|
|
974
|
+
"In ready state, shows search-filters, results-list with ListingCard grid, map-view; hides Spinner.",
|
|
975
|
+
"In no-results state, shows search-filters, empty state in results-list with Alert info; hides map-view.",
|
|
976
|
+
"In error state, shows search-filters, Alert destructive in results-list with retry; hides map-view and results."
|
|
977
|
+
],
|
|
978
|
+
"interaction": [
|
|
979
|
+
"Submitting search calls onSearchSubmit, transitioning to loading; derives from 'Search' label + transitionTo.",
|
|
980
|
+
"Clearing filters calls onClearFilters, transitioning to loading.",
|
|
981
|
+
"Viewing listing navigates to select-dates-book."
|
|
982
|
+
],
|
|
983
|
+
"styling": [
|
|
984
|
+
"Main layout uses `flex flex-col min-h-screen bg-background`.",
|
|
985
|
+
"Search filters header uses `sticky top-0 z-[200] bg-background border-b border-border p-4 shadow-sm`.",
|
|
986
|
+
"Results grid uses `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6`.",
|
|
987
|
+
"Map aside uses `sticky top-20 h-[calc(100vh-5rem)] w-full lg:w-1/3 border-l border-border` with `z-[100]`.",
|
|
988
|
+
"Page enter uses react-spring `useSpring({ opacity: isReady ? 1 : 0, config: { tension: 400, friction: 26 } })`; respects `useReducedMotion()` by setting `immediate: true`.",
|
|
989
|
+
"Loading skeleton for grid uses `h-64 w-full bg-muted animate-pulse` with explicit dimensions matching ListingCard.",
|
|
990
|
+
"No-results alert centered with `m-auto max-w-md`.",
|
|
991
|
+
"Error alert uses `m-6`.",
|
|
992
|
+
"Search button uses `variant=\"default\"` with `min-h-11`.",
|
|
993
|
+
"Clear button uses `variant=\"ghost\"`.",
|
|
994
|
+
"Hover on cards wrapped in `@media (hover: hover) and (pointer: fine) { hover:shadow-md }`.",
|
|
995
|
+
"Focus rings on inputs use `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
996
|
+
"Pagination at bottom uses `flex justify-center p-6 border-t border-border`.",
|
|
997
|
+
"Touch targets for filters use `min-h-11`."
|
|
998
|
+
]
|
|
999
|
+
},
|
|
1000
|
+
"props": [
|
|
1001
|
+
{
|
|
1002
|
+
"name": "isLoading",
|
|
1003
|
+
"type": "boolean",
|
|
1004
|
+
"required": false,
|
|
1005
|
+
"default": "false",
|
|
1006
|
+
"description": "Controls loading state.",
|
|
1007
|
+
"category": "state"
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
"name": "error",
|
|
1011
|
+
"type": "string | null",
|
|
1012
|
+
"required": false,
|
|
1013
|
+
"default": "null",
|
|
1014
|
+
"description": "Error message for error state.",
|
|
1015
|
+
"category": "state"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"name": "onSearchSubmit",
|
|
1019
|
+
"type": "(params: { location: string; dates: { from: Date; to: Date }; guests: number }) => void",
|
|
1020
|
+
"required": true,
|
|
1021
|
+
"description": "Callback for search submission.",
|
|
1022
|
+
"category": "callback"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
"name": "onClearFilters",
|
|
1026
|
+
"type": "() => void",
|
|
1027
|
+
"required": true,
|
|
1028
|
+
"description": "Callback to clear filters.",
|
|
1029
|
+
"category": "callback"
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
"name": "onRetry",
|
|
1033
|
+
"type": "() => void",
|
|
1034
|
+
"required": false,
|
|
1035
|
+
"description": "Retry callback in error state.",
|
|
1036
|
+
"category": "callback"
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
"storyVariants": [
|
|
1040
|
+
{
|
|
1041
|
+
"name": "Default",
|
|
1042
|
+
"description": "Page in ready state with results.",
|
|
1043
|
+
"args": {
|
|
1044
|
+
"isLoading": false,
|
|
1045
|
+
"error": null,
|
|
1046
|
+
"onSearchSubmit": "fn()",
|
|
1047
|
+
"onClearFilters": "fn()",
|
|
1048
|
+
"onRetry": "fn()"
|
|
1049
|
+
},
|
|
1050
|
+
"needsPlayFunction": true,
|
|
1051
|
+
"playDescription": "1. Type location with userEvent.type(getByPlaceholderText('Search location'), 'nyc'); 2. Click search with userEvent.click(getByRole('button', { name: 'Search' }));"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"name": "Loading",
|
|
1055
|
+
"description": "Page in loading state.",
|
|
1056
|
+
"args": {
|
|
1057
|
+
"isLoading": true,
|
|
1058
|
+
"error": null,
|
|
1059
|
+
"onSearchSubmit": "fn()",
|
|
1060
|
+
"onClearFilters": "fn()",
|
|
1061
|
+
"onRetry": "fn()"
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
"name": "NoResults",
|
|
1066
|
+
"description": "Page in no-results state.",
|
|
1067
|
+
"args": {
|
|
1068
|
+
"isLoading": false,
|
|
1069
|
+
"error": null,
|
|
1070
|
+
"onSearchSubmit": "fn()",
|
|
1071
|
+
"onClearFilters": "fn()",
|
|
1072
|
+
"onRetry": "fn()"
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
"name": "Error",
|
|
1077
|
+
"description": "Page in error state with retry.",
|
|
1078
|
+
"args": {
|
|
1079
|
+
"isLoading": false,
|
|
1080
|
+
"error": "Failed to load listings.",
|
|
1081
|
+
"onSearchSubmit": "fn()",
|
|
1082
|
+
"onClearFilters": "fn()",
|
|
1083
|
+
"onRetry": "fn()"
|
|
1084
|
+
},
|
|
1085
|
+
"needsPlayFunction": true,
|
|
1086
|
+
"playDescription": "Click retry with userEvent.click(getByRole('button', { name: 'Retry' }));"
|
|
1087
|
+
}
|
|
1088
|
+
],
|
|
1089
|
+
"dataContract": {
|
|
1090
|
+
"source": "graphql-query",
|
|
1091
|
+
"operationName": "SearchListings",
|
|
1092
|
+
"fields": [
|
|
1093
|
+
"listings.id",
|
|
1094
|
+
"listings.title",
|
|
1095
|
+
"listings.photo",
|
|
1096
|
+
"listings.price",
|
|
1097
|
+
"listings.location",
|
|
1098
|
+
"listings.availableDates"
|
|
1099
|
+
]
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
"componentId": "select-dates-book-page",
|
|
1104
|
+
"componentName": "SelectDatesBookPage",
|
|
1105
|
+
"isNew": true,
|
|
1106
|
+
"atomicType": "page",
|
|
1107
|
+
"composes": [
|
|
1108
|
+
"ui-components-calendar",
|
|
1109
|
+
"ui-components-select",
|
|
1110
|
+
"ui-components-carousel",
|
|
1111
|
+
"price-breakdown",
|
|
1112
|
+
"ui-components-form",
|
|
1113
|
+
"ui-components-alert",
|
|
1114
|
+
"ui-components-spinner",
|
|
1115
|
+
"ui-components-alertdialog"
|
|
1116
|
+
],
|
|
1117
|
+
"specDeltas": {
|
|
1118
|
+
"structure": ["<main> with listing-detail in article, booking-form in section, booking-summary in aside."],
|
|
1119
|
+
"rendering": [
|
|
1120
|
+
"In loading state, shows Spinner in all regions; hides feedback.",
|
|
1121
|
+
"In ready state, shows all regions without errors.",
|
|
1122
|
+
"In editing-invalid state, shows all regions with inline validation errors in booking-form.",
|
|
1123
|
+
"In submitting state, shows listing-detail and booking-summary; shows progress in booking-form region; hides booking-form inputs.",
|
|
1124
|
+
"In error state, shows listing-detail and booking-form; shows Alert destructive with retry in booking-summary; hides booking-summary content."
|
|
1125
|
+
],
|
|
1126
|
+
"interaction": [
|
|
1127
|
+
"Submitting booking validates guards (no past dates, no overlaps), calls onSubmitBooking if valid, transitioning to submitting; on success navigates to process-payment.",
|
|
1128
|
+
"Canceling navigates back to search-browse-listings.",
|
|
1129
|
+
"Unsaved changes triggers AlertDialog confirmation before navigating."
|
|
1130
|
+
],
|
|
1131
|
+
"styling": [
|
|
1132
|
+
"Main layout uses `flex flex-col md:flex-row min-h-screen bg-background gap-6 p-6`.",
|
|
1133
|
+
"Listing detail article uses `flex-1`.",
|
|
1134
|
+
"Booking form section uses `w-full md:w-96 sticky top-20 bg-background border border-border rounded-lg p-6 shadow-sm` with `z-[200]`.",
|
|
1135
|
+
"Booking summary aside uses `w-full md:w-80`.",
|
|
1136
|
+
"Page enter uses react-spring `useSpring({ opacity: isReady ? 1 : 0, config: { tension: 400, friction: 26 } })`; respects `useReducedMotion()`.",
|
|
1137
|
+
"Submitting progress uses `h-2 bg-secondary rounded-full` with react-spring `useSpring({ width: '0%' to '100%', config: { tension: 120, friction: 14 } })`; immediate on reduced motion.",
|
|
1138
|
+
"Error alert uses `mb-4`.",
|
|
1139
|
+
"Back button uses `variant=\"ghost\"` with `mb-4`.",
|
|
1140
|
+
"Submit button uses `variant=\"default\"` with `w-full min-h-11`.",
|
|
1141
|
+
"Cancel button uses `variant=\"ghost\"` with `w-full`.",
|
|
1142
|
+
"Hover on buttons wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }`.",
|
|
1143
|
+
"Focus rings on form elements use `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
1144
|
+
"Image gallery uses `mb-6`.",
|
|
1145
|
+
"Price numbers use `font-variant-numeric: tabular-nums`.",
|
|
1146
|
+
"Touch targets for selects use `min-h-11`."
|
|
1147
|
+
]
|
|
1148
|
+
},
|
|
1149
|
+
"props": [
|
|
1150
|
+
{
|
|
1151
|
+
"name": "isLoading",
|
|
1152
|
+
"type": "boolean",
|
|
1153
|
+
"required": false,
|
|
1154
|
+
"default": "false",
|
|
1155
|
+
"description": "Controls loading state.",
|
|
1156
|
+
"category": "state"
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
"name": "isSubmitting",
|
|
1160
|
+
"type": "boolean",
|
|
1161
|
+
"required": false,
|
|
1162
|
+
"default": "false",
|
|
1163
|
+
"description": "Controls submitting state.",
|
|
1164
|
+
"category": "state"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
"name": "error",
|
|
1168
|
+
"type": "string | null",
|
|
1169
|
+
"required": false,
|
|
1170
|
+
"default": "null",
|
|
1171
|
+
"description": "Error message for error state.",
|
|
1172
|
+
"category": "state"
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
"name": "onSubmitBooking",
|
|
1176
|
+
"type": "(data: { dates: { from: Date; to: Date }; guests: number }) => void",
|
|
1177
|
+
"required": true,
|
|
1178
|
+
"description": "Callback for booking submission.",
|
|
1179
|
+
"category": "callback"
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
"name": "onCancel",
|
|
1183
|
+
"type": "() => void",
|
|
1184
|
+
"required": true,
|
|
1185
|
+
"description": "Callback for canceling.",
|
|
1186
|
+
"category": "callback"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
"name": "onRetry",
|
|
1190
|
+
"type": "() => void",
|
|
1191
|
+
"required": false,
|
|
1192
|
+
"description": "Retry callback in error state.",
|
|
1193
|
+
"category": "callback"
|
|
1194
|
+
}
|
|
1195
|
+
],
|
|
1196
|
+
"storyVariants": [
|
|
1197
|
+
{
|
|
1198
|
+
"name": "Default",
|
|
1199
|
+
"description": "Page in ready state.",
|
|
1200
|
+
"args": {
|
|
1201
|
+
"isLoading": false,
|
|
1202
|
+
"isSubmitting": false,
|
|
1203
|
+
"error": null,
|
|
1204
|
+
"onSubmitBooking": "fn()",
|
|
1205
|
+
"onCancel": "fn()",
|
|
1206
|
+
"onRetry": "fn()"
|
|
1207
|
+
},
|
|
1208
|
+
"needsPlayFunction": true,
|
|
1209
|
+
"playDescription": "1. Select dates; 2. Click submit with userEvent.click(getByRole('button', { name: 'Request to book' }));"
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
"name": "Loading",
|
|
1213
|
+
"description": "Page in loading state.",
|
|
1214
|
+
"args": {
|
|
1215
|
+
"isLoading": true,
|
|
1216
|
+
"isSubmitting": false,
|
|
1217
|
+
"error": null,
|
|
1218
|
+
"onSubmitBooking": "fn()",
|
|
1219
|
+
"onCancel": "fn()",
|
|
1220
|
+
"onRetry": "fn()"
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
"name": "Submitting",
|
|
1225
|
+
"description": "Page in submitting state.",
|
|
1226
|
+
"args": {
|
|
1227
|
+
"isLoading": false,
|
|
1228
|
+
"isSubmitting": true,
|
|
1229
|
+
"error": null,
|
|
1230
|
+
"onSubmitBooking": "fn()",
|
|
1231
|
+
"onCancel": "fn()",
|
|
1232
|
+
"onRetry": "fn()"
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"name": "Error",
|
|
1237
|
+
"description": "Page in error state.",
|
|
1238
|
+
"args": {
|
|
1239
|
+
"isLoading": false,
|
|
1240
|
+
"isSubmitting": false,
|
|
1241
|
+
"error": "Booking failed.",
|
|
1242
|
+
"onSubmitBooking": "fn()",
|
|
1243
|
+
"onCancel": "fn()",
|
|
1244
|
+
"onRetry": "fn()"
|
|
1245
|
+
},
|
|
1246
|
+
"needsPlayFunction": true,
|
|
1247
|
+
"playDescription": "Click retry with userEvent.click(getByRole('button', { name: 'Retry' }));"
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
"name": "Invalid",
|
|
1251
|
+
"description": "Page in editing-invalid state.",
|
|
1252
|
+
"args": {
|
|
1253
|
+
"isLoading": false,
|
|
1254
|
+
"isSubmitting": false,
|
|
1255
|
+
"error": null,
|
|
1256
|
+
"onSubmitBooking": "fn()",
|
|
1257
|
+
"onCancel": "fn()",
|
|
1258
|
+
"onRetry": "fn()"
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
],
|
|
1262
|
+
"dataContract": {
|
|
1263
|
+
"source": "graphql-mutation",
|
|
1264
|
+
"operationName": "CreateBooking",
|
|
1265
|
+
"fields": ["listingId", "dates.from", "dates.to", "guests"]
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
"componentId": "process-payment-page",
|
|
1270
|
+
"componentName": "ProcessPaymentPage",
|
|
1271
|
+
"isNew": true,
|
|
1272
|
+
"atomicType": "page",
|
|
1273
|
+
"composes": [
|
|
1274
|
+
"credit-card-input",
|
|
1275
|
+
"ui-components-form",
|
|
1276
|
+
"ui-components-select",
|
|
1277
|
+
"price-breakdown",
|
|
1278
|
+
"ui-components-spinner",
|
|
1279
|
+
"ui-components-alert",
|
|
1280
|
+
"ui-components-alertdialog"
|
|
1281
|
+
],
|
|
1282
|
+
"specDeltas": {
|
|
1283
|
+
"structure": ["<main> with booking-confirmation in section, payment-form in form, payment-summary in aside."],
|
|
1284
|
+
"rendering": [
|
|
1285
|
+
"In loading state, shows Spinner in all regions.",
|
|
1286
|
+
"In ready state, shows all regions without errors.",
|
|
1287
|
+
"In editing-invalid state, shows all regions with inline errors in payment-form.",
|
|
1288
|
+
"In submitting state, shows booking-confirmation and payment-summary; shows progress in payment-form; hides inputs.",
|
|
1289
|
+
"In error state, shows booking-confirmation and payment-form; shows Alert destructive with retry in payment-summary."
|
|
1290
|
+
],
|
|
1291
|
+
"interaction": [
|
|
1292
|
+
"Submitting payment validates card and address, calls onPay if valid, transitioning to submitting.",
|
|
1293
|
+
"Canceling navigates back to select-dates-book.",
|
|
1294
|
+
"Unsaved changes triggers AlertDialog before navigating."
|
|
1295
|
+
],
|
|
1296
|
+
"styling": [
|
|
1297
|
+
"Main layout uses `flex flex-col md:flex-row min-h-screen bg-background gap-6 p-6`.",
|
|
1298
|
+
"Booking confirmation section uses `flex-1`.",
|
|
1299
|
+
"Payment form uses `w-full md:w-96 sticky top-20 bg-background border border-border rounded-lg p-6 shadow-sm` with `z-[200]`.",
|
|
1300
|
+
"Payment summary aside uses `w-full md:w-80`.",
|
|
1301
|
+
"Page enter uses react-spring `useSpring({ opacity: isReady ? 1 : 0, config: { tension: 400, friction: 26 } })`; respects `useReducedMotion()`.",
|
|
1302
|
+
"Submitting progress uses `h-2 bg-secondary rounded-full` with react-spring `useSpring({ width: '0%' to '100%', config: { tension: 120, friction: 14 } })`; immediate on reduced motion.",
|
|
1303
|
+
"Error alert uses `mb-4`.",
|
|
1304
|
+
"Pay button uses `variant=\"default\"` with `w-full min-h-11`.",
|
|
1305
|
+
"Cancel button uses `variant=\"ghost\"` with `w-full`.",
|
|
1306
|
+
"Hover on buttons wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }`.",
|
|
1307
|
+
"Focus rings on inputs use `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`.",
|
|
1308
|
+
"Card inputs use `mb-4`.",
|
|
1309
|
+
"Price numbers use `font-variant-numeric: tabular-nums`.",
|
|
1310
|
+
"Touch targets for form fields use `min-h-11`."
|
|
1311
|
+
]
|
|
1312
|
+
},
|
|
1313
|
+
"props": [
|
|
1314
|
+
{
|
|
1315
|
+
"name": "isLoading",
|
|
1316
|
+
"type": "boolean",
|
|
1317
|
+
"required": false,
|
|
1318
|
+
"default": "false",
|
|
1319
|
+
"description": "Controls loading state.",
|
|
1320
|
+
"category": "state"
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
"name": "isSubmitting",
|
|
1324
|
+
"type": "boolean",
|
|
1325
|
+
"required": false,
|
|
1326
|
+
"default": "false",
|
|
1327
|
+
"description": "Controls submitting state.",
|
|
1328
|
+
"category": "state"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
"name": "error",
|
|
1332
|
+
"type": "string | null",
|
|
1333
|
+
"required": false,
|
|
1334
|
+
"default": "null",
|
|
1335
|
+
"description": "Error message.",
|
|
1336
|
+
"category": "state"
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
"name": "onPay",
|
|
1340
|
+
"type": "(data: { card: object; address: object }) => void",
|
|
1341
|
+
"required": true,
|
|
1342
|
+
"description": "Callback for payment submission.",
|
|
1343
|
+
"category": "callback"
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
"name": "onCancel",
|
|
1347
|
+
"type": "() => void",
|
|
1348
|
+
"required": true,
|
|
1349
|
+
"description": "Callback for canceling.",
|
|
1350
|
+
"category": "callback"
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
"name": "onRetry",
|
|
1354
|
+
"type": "() => void",
|
|
1355
|
+
"required": false,
|
|
1356
|
+
"description": "Retry callback.",
|
|
1357
|
+
"category": "callback"
|
|
1358
|
+
}
|
|
1359
|
+
],
|
|
1360
|
+
"storyVariants": [
|
|
1361
|
+
{
|
|
1362
|
+
"name": "Default",
|
|
1363
|
+
"description": "Page in ready state.",
|
|
1364
|
+
"args": {
|
|
1365
|
+
"isLoading": false,
|
|
1366
|
+
"isSubmitting": false,
|
|
1367
|
+
"error": null,
|
|
1368
|
+
"onPay": "fn()",
|
|
1369
|
+
"onCancel": "fn()",
|
|
1370
|
+
"onRetry": "fn()"
|
|
1371
|
+
},
|
|
1372
|
+
"needsPlayFunction": true,
|
|
1373
|
+
"playDescription": "1. Fill card details; 2. Click pay with userEvent.click(getByRole('button', { name: /Pay/ }));"
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
"name": "Loading",
|
|
1377
|
+
"description": "Page in loading state.",
|
|
1378
|
+
"args": {
|
|
1379
|
+
"isLoading": true,
|
|
1380
|
+
"isSubmitting": false,
|
|
1381
|
+
"error": null,
|
|
1382
|
+
"onPay": "fn()",
|
|
1383
|
+
"onCancel": "fn()",
|
|
1384
|
+
"onRetry": "fn()"
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
"name": "Submitting",
|
|
1389
|
+
"description": "Page in submitting state.",
|
|
1390
|
+
"args": {
|
|
1391
|
+
"isLoading": false,
|
|
1392
|
+
"isSubmitting": true,
|
|
1393
|
+
"error": null,
|
|
1394
|
+
"onPay": "fn()",
|
|
1395
|
+
"onCancel": "fn()",
|
|
1396
|
+
"onRetry": "fn()"
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
"name": "Error",
|
|
1401
|
+
"description": "Page in error state.",
|
|
1402
|
+
"args": {
|
|
1403
|
+
"isLoading": false,
|
|
1404
|
+
"isSubmitting": false,
|
|
1405
|
+
"error": "Payment failed.",
|
|
1406
|
+
"onPay": "fn()",
|
|
1407
|
+
"onCancel": "fn()",
|
|
1408
|
+
"onRetry": "fn()"
|
|
1409
|
+
},
|
|
1410
|
+
"needsPlayFunction": true,
|
|
1411
|
+
"playDescription": "Click retry with userEvent.click(getByRole('button', { name: 'Retry' }));"
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
"name": "Invalid",
|
|
1415
|
+
"description": "Page in editing-invalid state.",
|
|
1416
|
+
"args": {
|
|
1417
|
+
"isLoading": false,
|
|
1418
|
+
"isSubmitting": false,
|
|
1419
|
+
"error": null,
|
|
1420
|
+
"onPay": "fn()",
|
|
1421
|
+
"onCancel": "fn()",
|
|
1422
|
+
"onRetry": "fn()"
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
],
|
|
1426
|
+
"dataContract": {
|
|
1427
|
+
"source": "graphql-mutation",
|
|
1428
|
+
"operationName": "ProcessPayment",
|
|
1429
|
+
"fields": ["bookingId", "card.number", "card.expiry", "card.cvv", "address.street", "address.city"]
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
]
|