@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.
Files changed (233) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +92 -0
  5. package/dist/src/commands/implement-component.d.ts +19 -0
  6. package/dist/src/commands/implement-component.d.ts.map +1 -1
  7. package/dist/src/commands/implement-component.js +109 -30
  8. package/dist/src/commands/implement-component.js.map +1 -1
  9. package/dist/src/commands/implement-component.test.js +259 -69
  10. package/dist/src/commands/implement-component.test.js.map +1 -1
  11. package/dist/src/extract-exports.d.ts +6 -0
  12. package/dist/src/extract-exports.d.ts.map +1 -0
  13. package/dist/src/extract-exports.js +46 -0
  14. package/dist/src/extract-exports.js.map +1 -0
  15. package/dist/src/generate-story-deterministic.d.ts +30 -0
  16. package/dist/src/generate-story-deterministic.d.ts.map +1 -0
  17. package/dist/src/generate-story-deterministic.js +229 -0
  18. package/dist/src/generate-story-deterministic.js.map +1 -0
  19. package/dist/src/index.d.ts +4 -0
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/index.js.map +1 -1
  23. package/dist/src/pipeline/run-pipeline.d.ts +69 -0
  24. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -0
  25. package/dist/src/pipeline/run-pipeline.js +78 -0
  26. package/dist/src/pipeline/run-pipeline.js.map +1 -0
  27. package/dist/src/pipeline/run-pipeline.test.d.ts +2 -0
  28. package/dist/src/pipeline/run-pipeline.test.d.ts.map +1 -0
  29. package/dist/src/pipeline/run-pipeline.test.js +247 -0
  30. package/dist/src/pipeline/run-pipeline.test.js.map +1 -0
  31. package/dist/src/pipeline/steps/generate-component.d.ts +4 -0
  32. package/dist/src/pipeline/steps/generate-component.d.ts.map +1 -0
  33. package/dist/src/pipeline/steps/generate-component.js +50 -0
  34. package/dist/src/pipeline/steps/generate-component.js.map +1 -0
  35. package/dist/src/pipeline/steps/generate-component.test.d.ts.map +1 -0
  36. package/dist/src/pipeline/steps/generate-component.test.js +106 -0
  37. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -0
  38. package/dist/src/pipeline/steps/generate-story.d.ts +3 -0
  39. package/dist/src/pipeline/steps/generate-story.d.ts.map +1 -0
  40. package/dist/src/pipeline/steps/generate-story.js +14 -0
  41. package/dist/src/pipeline/steps/generate-story.js.map +1 -0
  42. package/dist/src/pipeline/steps/generate-story.test.d.ts.map +1 -0
  43. package/dist/src/pipeline/steps/generate-story.test.js +41 -0
  44. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -0
  45. package/dist/src/pipeline/steps/generate-test.d.ts +4 -0
  46. package/dist/src/pipeline/steps/generate-test.d.ts.map +1 -0
  47. package/dist/src/pipeline/steps/generate-test.js +19 -0
  48. package/dist/src/pipeline/steps/generate-test.js.map +1 -0
  49. package/dist/src/pipeline/steps/generate-test.test.d.ts.map +1 -0
  50. package/dist/src/pipeline/steps/generate-test.test.js +60 -0
  51. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -0
  52. package/dist/src/pipeline/steps/lint-fix-loop.d.ts +4 -0
  53. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -0
  54. package/dist/src/pipeline/steps/lint-fix-loop.js +45 -0
  55. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -0
  56. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts +2 -0
  57. package/dist/src/pipeline/steps/lint-fix-loop.test.d.ts.map +1 -0
  58. package/dist/src/pipeline/steps/lint-fix-loop.test.js +119 -0
  59. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -0
  60. package/dist/src/pipeline/steps/story-fix-loop.d.ts +4 -0
  61. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -0
  62. package/dist/src/pipeline/steps/story-fix-loop.js +34 -0
  63. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -0
  64. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts +2 -0
  65. package/dist/src/pipeline/steps/story-fix-loop.test.d.ts.map +1 -0
  66. package/dist/src/pipeline/steps/story-fix-loop.test.js +94 -0
  67. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -0
  68. package/dist/src/pipeline/steps/storybook-test.d.ts +3 -0
  69. package/dist/src/pipeline/steps/storybook-test.d.ts.map +1 -0
  70. package/dist/src/pipeline/steps/storybook-test.js +22 -0
  71. package/dist/src/pipeline/steps/storybook-test.js.map +1 -0
  72. package/dist/src/pipeline/steps/storybook-test.test.d.ts +2 -0
  73. package/dist/src/pipeline/steps/storybook-test.test.d.ts.map +1 -0
  74. package/dist/src/pipeline/steps/storybook-test.test.js +66 -0
  75. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -0
  76. package/dist/src/pipeline/steps/test-fix-loop.d.ts +4 -0
  77. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -0
  78. package/dist/src/pipeline/steps/test-fix-loop.js +44 -0
  79. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -0
  80. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts +2 -0
  81. package/dist/src/pipeline/steps/test-fix-loop.test.d.ts.map +1 -0
  82. package/dist/src/pipeline/steps/test-fix-loop.test.js +168 -0
  83. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -0
  84. package/dist/src/pipeline/steps/type-fix-loop.d.ts +4 -0
  85. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -0
  86. package/dist/src/pipeline/steps/type-fix-loop.js +43 -0
  87. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -0
  88. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts +2 -0
  89. package/dist/src/pipeline/steps/type-fix-loop.test.d.ts.map +1 -0
  90. package/dist/src/pipeline/steps/type-fix-loop.test.js +112 -0
  91. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -0
  92. package/dist/src/pipeline/steps/visual-test.d.ts +3 -0
  93. package/dist/src/pipeline/steps/visual-test.d.ts.map +1 -0
  94. package/dist/src/pipeline/steps/visual-test.js +4 -0
  95. package/dist/src/pipeline/steps/visual-test.js.map +1 -0
  96. package/dist/src/pipeline/steps/visual-test.test.d.ts +2 -0
  97. package/dist/src/pipeline/steps/visual-test.test.d.ts.map +1 -0
  98. package/dist/src/pipeline/steps/visual-test.test.js +9 -0
  99. package/dist/src/pipeline/steps/visual-test.test.js.map +1 -0
  100. package/dist/src/project-context.d.ts +10 -0
  101. package/dist/src/project-context.d.ts.map +1 -0
  102. package/dist/src/project-context.js +178 -0
  103. package/dist/src/project-context.js.map +1 -0
  104. package/dist/src/prompt.d.ts +39 -7
  105. package/dist/src/prompt.d.ts.map +1 -1
  106. package/dist/src/prompt.js +233 -23
  107. package/dist/src/prompt.js.map +1 -1
  108. package/dist/src/prompt.test.js +154 -9
  109. package/dist/src/prompt.test.js.map +1 -1
  110. package/dist/src/scaffold.d.ts +49 -0
  111. package/dist/src/scaffold.d.ts.map +1 -0
  112. package/dist/src/scaffold.js +208 -0
  113. package/dist/src/scaffold.js.map +1 -0
  114. package/dist/src/tools/lint-runner.d.ts +7 -0
  115. package/dist/src/tools/lint-runner.d.ts.map +1 -0
  116. package/dist/src/tools/lint-runner.js +48 -0
  117. package/dist/src/tools/lint-runner.js.map +1 -0
  118. package/dist/src/tools/lint-runner.test.d.ts +2 -0
  119. package/dist/src/tools/lint-runner.test.d.ts.map +1 -0
  120. package/dist/src/tools/lint-runner.test.js +90 -0
  121. package/dist/src/tools/lint-runner.test.js.map +1 -0
  122. package/dist/src/tools/storybook-runner.d.ts +6 -0
  123. package/dist/src/tools/storybook-runner.d.ts.map +1 -0
  124. package/dist/src/tools/storybook-runner.js +25 -0
  125. package/dist/src/tools/storybook-runner.js.map +1 -0
  126. package/dist/src/tools/storybook-runner.test.d.ts +2 -0
  127. package/dist/src/tools/storybook-runner.test.d.ts.map +1 -0
  128. package/dist/src/tools/storybook-runner.test.js +43 -0
  129. package/dist/src/tools/storybook-runner.test.js.map +1 -0
  130. package/dist/src/tools/test-runner.d.ts +9 -0
  131. package/dist/src/tools/test-runner.d.ts.map +1 -0
  132. package/dist/src/tools/test-runner.js +74 -0
  133. package/dist/src/tools/test-runner.js.map +1 -0
  134. package/dist/src/tools/test-runner.test.d.ts +2 -0
  135. package/dist/src/tools/test-runner.test.d.ts.map +1 -0
  136. package/dist/src/tools/test-runner.test.js +177 -0
  137. package/dist/src/tools/test-runner.test.js.map +1 -0
  138. package/dist/src/tools/type-checker.d.ts +6 -0
  139. package/dist/src/tools/type-checker.d.ts.map +1 -0
  140. package/dist/src/tools/type-checker.js +36 -0
  141. package/dist/src/tools/type-checker.js.map +1 -0
  142. package/dist/src/tools/type-checker.test.d.ts +2 -0
  143. package/dist/src/tools/type-checker.test.d.ts.map +1 -0
  144. package/dist/src/tools/type-checker.test.js +96 -0
  145. package/dist/src/tools/type-checker.test.js.map +1 -0
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/inputs/model-a/spec-deltas.json +1460 -0
  148. package/inputs/model-b/spec-deltas.json +1424 -0
  149. package/inputs/model-c/spec-deltas.json +1432 -0
  150. package/inputs/model-d/spec-deltas.json +967 -0
  151. package/inputs/model-e/spec-deltas.json +2292 -0
  152. package/ketchup-plan.md +43 -8
  153. package/package.json +3 -3
  154. package/scoring-heuristic.md +138 -0
  155. package/scripts/improve.ts +23 -18
  156. package/src/commands/implement-component.test.ts +309 -76
  157. package/src/commands/implement-component.ts +155 -31
  158. package/src/extract-exports.ts +53 -0
  159. package/src/generate-story-deterministic.ts +267 -0
  160. package/src/index.ts +12 -0
  161. package/src/pipeline/run-pipeline.test.ts +292 -0
  162. package/src/pipeline/run-pipeline.ts +160 -0
  163. package/src/pipeline/steps/generate-component.test.ts +130 -0
  164. package/src/pipeline/steps/generate-component.ts +60 -0
  165. package/src/pipeline/steps/generate-story.test.ts +54 -0
  166. package/src/pipeline/steps/generate-story.ts +17 -0
  167. package/src/pipeline/steps/generate-test.test.ts +75 -0
  168. package/src/pipeline/steps/generate-test.ts +25 -0
  169. package/src/pipeline/steps/lint-fix-loop.test.ts +155 -0
  170. package/src/pipeline/steps/lint-fix-loop.ts +59 -0
  171. package/src/pipeline/steps/story-fix-loop.test.ts +123 -0
  172. package/src/pipeline/steps/story-fix-loop.ts +47 -0
  173. package/src/pipeline/steps/storybook-test.test.ts +82 -0
  174. package/src/pipeline/steps/storybook-test.ts +27 -0
  175. package/src/pipeline/steps/test-fix-loop.test.ts +201 -0
  176. package/src/pipeline/steps/test-fix-loop.ts +56 -0
  177. package/src/pipeline/steps/type-fix-loop.test.ts +145 -0
  178. package/src/pipeline/steps/type-fix-loop.ts +55 -0
  179. package/src/pipeline/steps/visual-test.test.ts +10 -0
  180. package/src/pipeline/steps/visual-test.ts +5 -0
  181. package/src/project-context.ts +205 -0
  182. package/src/prompt.test.ts +174 -8
  183. package/src/prompt.ts +301 -23
  184. package/src/scaffold.ts +281 -0
  185. package/src/tools/lint-runner.test.ts +112 -0
  186. package/src/tools/lint-runner.ts +52 -0
  187. package/src/tools/storybook-runner.test.ts +53 -0
  188. package/src/tools/storybook-runner.ts +29 -0
  189. package/src/tools/test-runner.test.ts +213 -0
  190. package/src/tools/test-runner.ts +84 -0
  191. package/src/tools/type-checker.test.ts +120 -0
  192. package/src/tools/type-checker.ts +42 -0
  193. package/vitest.config.ts +9 -1
  194. package/dist/src/generate-component.d.ts +0 -4
  195. package/dist/src/generate-component.d.ts.map +0 -1
  196. package/dist/src/generate-component.js +0 -14
  197. package/dist/src/generate-component.js.map +0 -1
  198. package/dist/src/generate-component.test.d.ts.map +0 -1
  199. package/dist/src/generate-component.test.js +0 -73
  200. package/dist/src/generate-component.test.js.map +0 -1
  201. package/dist/src/generate-story.d.ts +0 -4
  202. package/dist/src/generate-story.d.ts.map +0 -1
  203. package/dist/src/generate-story.js +0 -14
  204. package/dist/src/generate-story.js.map +0 -1
  205. package/dist/src/generate-story.test.d.ts.map +0 -1
  206. package/dist/src/generate-story.test.js +0 -58
  207. package/dist/src/generate-story.test.js.map +0 -1
  208. package/dist/src/generate-test.d.ts +0 -4
  209. package/dist/src/generate-test.d.ts.map +0 -1
  210. package/dist/src/generate-test.js +0 -14
  211. package/dist/src/generate-test.js.map +0 -1
  212. package/dist/src/generate-test.test.d.ts.map +0 -1
  213. package/dist/src/generate-test.test.js +0 -77
  214. package/dist/src/generate-test.test.js.map +0 -1
  215. package/dist/src/reconcile.d.ts +0 -8
  216. package/dist/src/reconcile.d.ts.map +0 -1
  217. package/dist/src/reconcile.js +0 -18
  218. package/dist/src/reconcile.js.map +0 -1
  219. package/dist/src/reconcile.test.d.ts +0 -2
  220. package/dist/src/reconcile.test.d.ts.map +0 -1
  221. package/dist/src/reconcile.test.js +0 -108
  222. package/dist/src/reconcile.test.js.map +0 -1
  223. package/src/generate-component.test.ts +0 -89
  224. package/src/generate-component.ts +0 -16
  225. package/src/generate-story.test.ts +0 -71
  226. package/src/generate-story.ts +0 -16
  227. package/src/generate-test.test.ts +0 -93
  228. package/src/generate-test.ts +0 -16
  229. package/src/reconcile.test.ts +0 -127
  230. package/src/reconcile.ts +0 -27
  231. /package/dist/src/{generate-component.test.d.ts → pipeline/steps/generate-component.test.d.ts} +0 -0
  232. /package/dist/src/{generate-story.test.d.ts → pipeline/steps/generate-story.test.d.ts} +0 -0
  233. /package/dist/src/{generate-test.test.d.ts → pipeline/steps/generate-test.test.d.ts} +0 -0
@@ -0,0 +1,967 @@
1
+ [
2
+ {
3
+ "componentId": "message-bubble",
4
+ "componentName": "MessageBubble",
5
+ "isNew": true,
6
+ "atomicType": "atom",
7
+ "composes": ["ui-components-avatar"],
8
+ "specDeltas": {
9
+ "structure": [
10
+ "Composes Avatar for sender image, with text content in a rounded bubble",
11
+ "Uses <article> semantic element for the message container",
12
+ "Sender name in <header> element",
13
+ "Message text in <p> element"
14
+ ],
15
+ "rendering": [
16
+ "Renders sender avatar if provided, otherwise a placeholder",
17
+ "Displays message text from data prop",
18
+ "Shows timestamp in muted text"
19
+ ],
20
+ "interaction": ["No interactive elements by default"],
21
+ "styling": [
22
+ "Message bubble container uses `flex items-start gap-3 p-3 rounded-lg bg-muted` for received messages, `flex items-start gap-3 p-3 rounded-lg bg-primary text-primary-foreground justify-end` for own messages with `flex-row-reverse`.",
23
+ "Sender avatar uses `h-8 w-8 rounded-full` from Avatar component, fallback to `bg-accent flex items-center justify-center text-accent-foreground font-medium text-sm` with tabular-nums if counts are displayed.",
24
+ "Sender name uses `text-sm font-semibold text-foreground`, timestamp uses `text-xs text-muted-foreground ml-auto` with `font-variant-numeric: tabular-nums` for consistent digit width.",
25
+ "Message text uses `text-sm text-foreground text-wrap balance` to prevent layout shifts in headings or short texts.",
26
+ "Hover effect on bubble wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-muted/80 }` with `transition: background-color 150ms ease`, respects `prefers-reduced-motion: reduce` by setting `transition: none`."
27
+ ]
28
+ },
29
+ "props": [
30
+ {
31
+ "name": "message",
32
+ "type": "{ id: string; sender: { name: string; avatar?: string }; text: string; timestamp: string }",
33
+ "required": true,
34
+ "description": "The message data to display.",
35
+ "category": "data"
36
+ },
37
+ {
38
+ "name": "isOwnMessage",
39
+ "type": "boolean",
40
+ "required": false,
41
+ "default": "false",
42
+ "description": "Whether this is the current user's message, affects alignment.",
43
+ "category": "state"
44
+ }
45
+ ],
46
+ "storyVariants": [
47
+ {
48
+ "name": "Default",
49
+ "description": "A standard received message.",
50
+ "args": {
51
+ "message": {
52
+ "id": "1",
53
+ "sender": {
54
+ "name": "John Doe",
55
+ "avatar": "/avatar.jpg"
56
+ },
57
+ "text": "Hello!",
58
+ "timestamp": "2024-03-15T10:00:00Z"
59
+ },
60
+ "isOwnMessage": false
61
+ },
62
+ "needsPlayFunction": false
63
+ },
64
+ {
65
+ "name": "OwnMessage",
66
+ "description": "A message sent by the current user.",
67
+ "args": {
68
+ "message": {
69
+ "id": "2",
70
+ "sender": {
71
+ "name": "Me"
72
+ },
73
+ "text": "Hi there!",
74
+ "timestamp": "2024-03-15T10:01:00Z"
75
+ },
76
+ "isOwnMessage": true
77
+ },
78
+ "needsPlayFunction": false
79
+ }
80
+ ],
81
+ "dataContract": {
82
+ "source": "props",
83
+ "propsFieldName": "message",
84
+ "fields": ["id", "sender.name", "sender.avatar", "text", "timestamp"]
85
+ }
86
+ },
87
+ {
88
+ "componentId": "multi-participant-selector",
89
+ "componentName": "MultiParticipantSelector",
90
+ "isNew": true,
91
+ "atomicType": "molecule",
92
+ "composes": ["ui-components-combobox", "ui-components-badge"],
93
+ "specDeltas": {
94
+ "structure": [
95
+ "Composes Combobox for searchable input, with selected participants as Badge list",
96
+ "Uses <section> for the selector container with role='group' and aria-label='Participant selection'",
97
+ "Selected badges in a horizontal flex wrap",
98
+ "Combobox for adding new participants"
99
+ ],
100
+ "rendering": [
101
+ "Renders list of selected participants as removable Badges",
102
+ "Combobox shows available participants not yet selected",
103
+ "In loading state (isLoading=true), shows Skeleton for input area",
104
+ "In error state (error non-null), shows Alert with error message and retry button"
105
+ ],
106
+ "interaction": [
107
+ "Selecting from Combobox adds to selected list and calls onChange with updated array",
108
+ "Clicking remove on a Badge removes it from selected and calls onChange",
109
+ "Calls onChange: (selected: Participant[]) => void on every selection change",
110
+ "On retry click in error state, calls onRetry: () => void"
111
+ ],
112
+ "styling": [
113
+ "Selector container uses `flex flex-col gap-4 p-4 border rounded-md bg-background`.",
114
+ "Selected badges list uses `flex flex-wrap gap-2`, each Badge with `variant=\"secondary\"` rendering as `bg-secondary text-secondary-foreground text-sm font-medium px-2.5 py-0.5 rounded` and remove X icon as `ml-1 h-4 w-4 inline-flex items-center justify-center rounded-full hover:bg-accent hover:text-accent-foreground transition: background-color 150ms ease` wrapped in `@media (hover: hover) and (pointer: fine)`, with min-h-11 min-w-11 for touch targets and touch-action: manipulation.",
115
+ "Combobox input uses `w-full h-10 px-3 py-2 rounded-md border border-input bg-background text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with `text-base` to prevent iOS zoom.",
116
+ "In loading state, skeleton uses `h-8 w-full bg-muted rounded-md` with CSS 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` to match input dimensions for CLS prevention.",
117
+ "In error state, Alert uses `variant=\"destructive\"` rendering as `border border-destructive text-destructive p-4 rounded-md` with text-balance on title.",
118
+ "Badges use useTransition(selected, { from: { opacity: 0, transform: 'scale(0.95)' }, enter: { opacity: 1, transform: 'scale(1)' }, leave: { opacity: 0, transform: 'scale(0.95)' }, config: { tension: 400, friction: 22 } }) for enter/exit animations, respects useReducedMotion() by setting immediate: true when reduced.",
119
+ "Retry button in error uses `variant=\"default\"` with `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md` and CSS `active:scale-[0.97] transition: transform 100ms ease-out`, hover wrapped in `@media (hover: hover) and (pointer: fine)`.",
120
+ "Focus outlines use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` on all interactive elements, using neutral colors only."
121
+ ]
122
+ },
123
+ "props": [
124
+ {
125
+ "name": "participants",
126
+ "type": "Array<{ id: string; name: string; avatar?: string }>",
127
+ "required": true,
128
+ "description": "List of available participants.",
129
+ "category": "data"
130
+ },
131
+ {
132
+ "name": "selected",
133
+ "type": "Array<{ id: string; name: string; avatar?: string }>",
134
+ "required": true,
135
+ "description": "Currently selected participants.",
136
+ "category": "data"
137
+ },
138
+ {
139
+ "name": "onChange",
140
+ "type": "(selected: Array<{ id: string; name: string; avatar?: string }>) => void",
141
+ "required": true,
142
+ "description": "Callback when selection changes.",
143
+ "category": "callback"
144
+ },
145
+ {
146
+ "name": "isLoading",
147
+ "type": "boolean",
148
+ "required": false,
149
+ "default": "false",
150
+ "description": "Whether the selector is in loading state.",
151
+ "category": "state"
152
+ },
153
+ {
154
+ "name": "error",
155
+ "type": "string | null",
156
+ "required": false,
157
+ "default": "null",
158
+ "description": "Error message if failed to load participants.",
159
+ "category": "state"
160
+ },
161
+ {
162
+ "name": "onRetry",
163
+ "type": "() => void",
164
+ "required": false,
165
+ "description": "Callback to retry loading participants.",
166
+ "category": "callback"
167
+ }
168
+ ],
169
+ "storyVariants": [
170
+ {
171
+ "name": "Default",
172
+ "description": "Selector with some selected participants.",
173
+ "args": {
174
+ "participants": [
175
+ {
176
+ "id": "1",
177
+ "name": "Alice"
178
+ },
179
+ {
180
+ "id": "2",
181
+ "name": "Bob"
182
+ },
183
+ {
184
+ "id": "3",
185
+ "name": "Charlie"
186
+ }
187
+ ],
188
+ "selected": [
189
+ {
190
+ "id": "1",
191
+ "name": "Alice"
192
+ }
193
+ ],
194
+ "onChange": "fn()"
195
+ },
196
+ "needsPlayFunction": true,
197
+ "playDescription": "1. Click the combobox input to open dropdown, 2. Type 'B' to filter, 3. Click 'Bob' option, 4. Verify new badge appears and onChange is called, 5. Click remove on Alice badge, 6. Verify badge removes and onChange called."
198
+ },
199
+ {
200
+ "name": "Loading",
201
+ "description": "Loading state with skeleton.",
202
+ "args": {
203
+ "participants": [],
204
+ "selected": [],
205
+ "onChange": "fn()",
206
+ "isLoading": true
207
+ },
208
+ "needsPlayFunction": false
209
+ },
210
+ {
211
+ "name": "Error",
212
+ "description": "Error state with retry button.",
213
+ "args": {
214
+ "participants": [],
215
+ "selected": [],
216
+ "onChange": "fn()",
217
+ "error": "Failed to load participants",
218
+ "onRetry": "fn()"
219
+ },
220
+ "needsPlayFunction": true,
221
+ "playDescription": "1. Find the retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. expect(onRetry).toHaveBeenCalled()."
222
+ }
223
+ ],
224
+ "dataContract": {
225
+ "source": "props",
226
+ "propsFieldName": "participants",
227
+ "fields": ["id", "name", "avatar"]
228
+ }
229
+ },
230
+ {
231
+ "componentId": "message-composer",
232
+ "componentName": "MessageComposer",
233
+ "isNew": true,
234
+ "atomicType": "molecule",
235
+ "composes": ["ui-components-textarea", "ui-components-button"],
236
+ "specDeltas": {
237
+ "structure": [
238
+ "Composes Textarea for message input and Button for send",
239
+ "Uses <form> element to wrap input and button for native Enter-to-submit",
240
+ "Textarea with inputMode='text' and autocomplete='off'",
241
+ "Send button as Button with variant='default'"
242
+ ],
243
+ "rendering": [
244
+ "In ready state, shows enabled Textarea and Send button",
245
+ "In sending state (isSending=true), disables Textarea and Button, shows Spinner inside Button",
246
+ "In send-error state (error non-null), shows Alert with error message and retry Button, hides recent-messages region",
247
+ "In editing-invalid state (invalid=true), shows inline error text below Textarea: 'Message cannot be empty'"
248
+ ],
249
+ "interaction": [
250
+ "On Textarea change, calls onMessageChange: (text: string) => void",
251
+ "On form submit (Enter or Send click), validates text non-empty; if valid calls onSend: (text: string) => void, else transitions to editing-invalid",
252
+ "On retry click in send-error, calls onRetry: () => void, transitions back to sending",
253
+ "Supports Cmd+Enter submission for multi-line input",
254
+ "Submit button disables during isSending to prevent duplicates",
255
+ "Uses controlled value prop for text, clears value after successful send via parent"
256
+ ],
257
+ "styling": [
258
+ "Form container uses `flex items-center gap-2 p-4 border-t bg-background`.",
259
+ "Textarea uses `flex-1 min-h-[80px] px-3 py-2 rounded-md border border-input bg-background text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 text-base resize-none` to prevent iOS zoom and allow multi-line input.",
260
+ "Send button uses `variant=\"default\"` rendering as `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md` with CSS `active:scale-[0.97] transition: transform 100ms ease-out`, hover wrapped in `@media (hover: hover) and (pointer: fine) { hover:bg-primary/90 }` with `transition: background-color 150ms ease`, min-h-11 min-w-11 for touch targets, touch-action: manipulation.",
261
+ "In sending state, button adds `opacity-50 cursor-not-allowed` and replaces text with Spinner using `animate-spin h-5 w-5 text-primary-foreground`.",
262
+ "In error state, Alert uses `variant=\"destructive\"` rendering as `border border-destructive text-destructive p-4 rounded-md w-full mt-2` with retry button as `bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2 rounded-md`.",
263
+ "Inline error text uses `text-sm text-destructive mt-1` with `text-wrap: balance`.",
264
+ "Retry button hover uses `transition: background-color 150ms ease` wrapped in `@media (hover: hover) and (pointer: fine)`, respects `prefers-reduced-motion: reduce` with `transition: none`.",
265
+ "Focus outlines on textarea and button use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with neutral colors."
266
+ ]
267
+ },
268
+ "props": [
269
+ {
270
+ "name": "value",
271
+ "type": "string",
272
+ "required": true,
273
+ "description": "Current message text (controlled).",
274
+ "category": "data"
275
+ },
276
+ {
277
+ "name": "onMessageChange",
278
+ "type": "(text: string) => void",
279
+ "required": true,
280
+ "description": "Callback on text change.",
281
+ "category": "callback"
282
+ },
283
+ {
284
+ "name": "onSend",
285
+ "type": "(text: string) => void",
286
+ "required": true,
287
+ "description": "Callback to send message.",
288
+ "category": "callback"
289
+ },
290
+ {
291
+ "name": "isSending",
292
+ "type": "boolean",
293
+ "required": false,
294
+ "default": "false",
295
+ "description": "Whether message is sending.",
296
+ "category": "state"
297
+ },
298
+ {
299
+ "name": "error",
300
+ "type": "string | null",
301
+ "required": false,
302
+ "default": "null",
303
+ "description": "Send error message.",
304
+ "category": "state"
305
+ },
306
+ {
307
+ "name": "onRetry",
308
+ "type": "() => void",
309
+ "required": false,
310
+ "description": "Callback to retry sending.",
311
+ "category": "callback"
312
+ }
313
+ ],
314
+ "storyVariants": [
315
+ {
316
+ "name": "Default",
317
+ "description": "Ready state with some text.",
318
+ "args": {
319
+ "value": "Hello",
320
+ "onMessageChange": "fn()",
321
+ "onSend": "fn()",
322
+ "isSending": false,
323
+ "error": null,
324
+ "onRetry": "fn()"
325
+ },
326
+ "needsPlayFunction": true,
327
+ "playDescription": "1. Find the textarea with getByRole('textbox'), 2. userEvent.type(textarea, ' world'), 3. expect(onMessageChange).toHaveBeenCalledWith('Hello world'), 4. Find submit button with getByRole('button', { name: 'Send' }), 5. userEvent.click(submitButton), 6. expect(onSend).toHaveBeenCalledWith('Hello world')."
328
+ },
329
+ {
330
+ "name": "Sending",
331
+ "description": "Sending state with disabled input.",
332
+ "args": {
333
+ "value": "Sending...",
334
+ "onMessageChange": "fn()",
335
+ "onSend": "fn()",
336
+ "isSending": true,
337
+ "error": null,
338
+ "onRetry": "fn()"
339
+ },
340
+ "needsPlayFunction": false
341
+ },
342
+ {
343
+ "name": "Error",
344
+ "description": "Error state with retry.",
345
+ "args": {
346
+ "value": "",
347
+ "onMessageChange": "fn()",
348
+ "onSend": "fn()",
349
+ "isSending": false,
350
+ "error": "Failed to send",
351
+ "onRetry": "fn()"
352
+ },
353
+ "needsPlayFunction": true,
354
+ "playDescription": "1. Find the retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. expect(onRetry).toHaveBeenCalled()."
355
+ },
356
+ {
357
+ "name": "Invalid",
358
+ "description": "Invalid state with empty message.",
359
+ "args": {
360
+ "value": "",
361
+ "onMessageChange": "fn()",
362
+ "onSend": "fn()",
363
+ "isSending": false,
364
+ "error": null,
365
+ "onRetry": "fn()"
366
+ },
367
+ "needsPlayFunction": true,
368
+ "playDescription": "1. Find submit button with getByRole('button', { name: 'Send' }), 2. userEvent.click(submitButton), 3. await waitFor(() => expect(getByText('Message cannot be empty')).toBeVisible())."
369
+ }
370
+ ],
371
+ "dataContract": {
372
+ "source": "local-state",
373
+ "fields": ["messageText"]
374
+ }
375
+ },
376
+ {
377
+ "componentId": "message-list",
378
+ "componentName": "MessageList",
379
+ "isNew": true,
380
+ "atomicType": "organism",
381
+ "composes": ["message-bubble", "ui-components-scrollarea"],
382
+ "specDeltas": {
383
+ "structure": [
384
+ "Composes ScrollArea for scrollable container, with MessageBubble for each message",
385
+ "Uses <ul> semantic element for the list with role='log' and aria-live='polite'",
386
+ "Each MessageBubble as <li> with unique key"
387
+ ],
388
+ "rendering": [
389
+ "In loading state (isLoading=true), shows Skeleton placeholders for 3-5 message bubbles, hides message-composer region",
390
+ "In ready state, renders messages array as vertical list, scrolls to bottom on new messages",
391
+ "In error state (error non-null), shows Alert with error message and retry button, hides message-list region",
392
+ "When new messages received (real-time), appends to list and scrolls to bottom smoothly"
393
+ ],
394
+ "interaction": [
395
+ "On component mount or new messages, calls scrollToBottom using scrollIntoView({ behavior: 'smooth' }) on last message",
396
+ "On retry click in error state, calls onRetry: () => void, transitions to loading"
397
+ ],
398
+ "styling": [
399
+ "List container uses `flex flex-col gap-4 p-4 overflow-y-auto h-full` inside ScrollArea with custom scrollbar `w-2 bg-border rounded-full` and thumb `bg-primary rounded-full`.",
400
+ "Each message uses useTransition(messages, { from: { opacity: 0, transform: 'translateY(8px)' }, enter: { opacity: 1, transform: 'translateY(0)' }, leave: { opacity: 0, transform: 'translateY(8px)' }, config: { tension: 300, friction: 22 } }) for staggered entry with useTrail(messages.length, ...), respects useReducedMotion() by setting immediate: true.",
401
+ "In loading state, skeletons use `h-16 w-full bg-muted rounded-lg` with CSS 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` repeated 5 times to match message dimensions for CLS prevention.",
402
+ "In error state, Alert uses `variant=\"destructive\"` rendering as `border border-destructive text-destructive p-4 rounded-md` centered with `m-4`.",
403
+ "Retry button uses `variant=\"default\"` with `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md` and CSS `active:scale-[0.97] transition: transform 100ms ease-out`, hover wrapped in `@media (hover: hover) and (pointer: fine)`.",
404
+ "Timestamp in messages uses `font-variant-numeric: tabular-nums` for consistent width, text-xs text-muted-foreground.",
405
+ "List uses `scroll-margin-top: 4rem` to account for sticky headers.",
406
+ "Error alert fades in using useTransition(error, { from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, config: { tension: 400, friction: 22 } }), same config for content, respects reduced motion with immediate: true.",
407
+ "Interactive elements like retry have min-h-11 min-w-11 for touch targets, touch-action: manipulation, hover behind `@media (hover: hover) and (pointer: fine)`.",
408
+ "Focus outlines use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with neutral colors."
409
+ ]
410
+ },
411
+ "props": [
412
+ {
413
+ "name": "messages",
414
+ "type": "Array<{ id: string; sender: { name: string; avatar?: string }; text: string; timestamp: string }>",
415
+ "required": true,
416
+ "description": "List of messages to display.",
417
+ "category": "data"
418
+ },
419
+ {
420
+ "name": "isLoading",
421
+ "type": "boolean",
422
+ "required": false,
423
+ "default": "false",
424
+ "description": "Whether messages are loading.",
425
+ "category": "state"
426
+ },
427
+ {
428
+ "name": "error",
429
+ "type": "string | null",
430
+ "required": false,
431
+ "default": "null",
432
+ "description": "Error message if failed to load.",
433
+ "category": "state"
434
+ },
435
+ {
436
+ "name": "onRetry",
437
+ "type": "() => void",
438
+ "required": false,
439
+ "description": "Callback to retry loading messages.",
440
+ "category": "callback"
441
+ }
442
+ ],
443
+ "storyVariants": [
444
+ {
445
+ "name": "Default",
446
+ "description": "List with sample messages.",
447
+ "args": {
448
+ "messages": [
449
+ {
450
+ "id": "1",
451
+ "sender": {
452
+ "name": "Alice"
453
+ },
454
+ "text": "Hi",
455
+ "timestamp": "2024-03-15T10:00:00Z"
456
+ },
457
+ {
458
+ "id": "2",
459
+ "sender": {
460
+ "name": "Bob"
461
+ },
462
+ "text": "Hello",
463
+ "timestamp": "2024-03-15T10:01:00Z"
464
+ }
465
+ ],
466
+ "isLoading": false,
467
+ "error": null,
468
+ "onRetry": "fn()"
469
+ },
470
+ "needsPlayFunction": false
471
+ },
472
+ {
473
+ "name": "Loading",
474
+ "description": "Loading state with skeletons.",
475
+ "args": {
476
+ "messages": [],
477
+ "isLoading": true,
478
+ "error": null,
479
+ "onRetry": "fn()"
480
+ },
481
+ "needsPlayFunction": false
482
+ },
483
+ {
484
+ "name": "Error",
485
+ "description": "Error state with retry.",
486
+ "args": {
487
+ "messages": [],
488
+ "isLoading": false,
489
+ "error": "Failed to load messages",
490
+ "onRetry": "fn()"
491
+ },
492
+ "needsPlayFunction": true,
493
+ "playDescription": "1. Find the retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. expect(onRetry).toHaveBeenCalled()."
494
+ }
495
+ ],
496
+ "dataContract": {
497
+ "source": "props",
498
+ "propsFieldName": "messages",
499
+ "fields": ["id", "sender.name", "sender.avatar", "text", "timestamp"]
500
+ }
501
+ },
502
+ {
503
+ "componentId": "chat-form",
504
+ "componentName": "ChatForm",
505
+ "isNew": true,
506
+ "atomicType": "organism",
507
+ "composes": ["ui-components-form", "ui-components-input", "multi-participant-selector", "ui-components-button"],
508
+ "specDeltas": {
509
+ "structure": [
510
+ "Composes Form with fields: Input for chat name, MultiParticipantSelector for participants, Button for submit and cancel",
511
+ "Uses <form> element with role='form' and aria-label='Create new chat'",
512
+ "Vertical stack layout with FormItems for name and participants"
513
+ ],
514
+ "rendering": [
515
+ "In loading state (isLoading=true), shows Skeleton for form fields and buttons, hides form region",
516
+ "In ready state, shows editable form fields and enabled buttons",
517
+ "In editing-invalid state, shows inline validation errors: for name 'Chat name is required', for participants 'At least one participant required'",
518
+ "In submitting state (isSubmitting=true), disables form fields and buttons, shows progress indicator in submit button",
519
+ "In error state (error non-null), shows Alert with error message and retry button, hides form region"
520
+ ],
521
+ "interaction": [
522
+ "On name change, calls onNameChange: (name: string) => void",
523
+ "On participants change, calls onParticipantsChange: (selected: Participant[]) => void",
524
+ "On submit click or Enter, validates name non-empty and participants >=1; if valid calls onSubmit: (data: { name: string; participants: Participant[] }) => void, transitions to submitting; else shows editing-invalid",
525
+ "On cancel click, checks for unsaved changes; if changes, shows confirmation AlertDialog: 'Unsaved changes will be lost. Continue?' with confirm calling onCancel: () => void",
526
+ "On retry in error, calls onRetry: () => void, transitions to loading",
527
+ "Validation on blur for name and on submit for all fields",
528
+ "Clear field-level errors when user starts editing that field"
529
+ ],
530
+ "styling": [
531
+ "Form container uses `flex flex-col gap-6 p-6 bg-background rounded-lg shadow-sm`.",
532
+ "Name input uses `h-10 w-full px-3 py-2 rounded-md border border-input bg-background text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 text-base` to prevent iOS zoom.",
533
+ "Participant selector uses `flex flex-col gap-2`, with badges `flex flex-wrap gap-2`.",
534
+ "Submit button uses `variant=\"default\"` rendering as `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md` with CSS `active:scale-[0.97] transition: transform 100ms ease-out`, min-h-11 min-w-11, touch-action: manipulation.",
535
+ "Cancel button uses `variant=\"ghost\"` rendering as `bg-transparent hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 rounded-md` with similar active and hover specs.",
536
+ "In submitting state, submit button adds `opacity-50 cursor-not-allowed` and shows Progress with useSpring({ width: '0%' to '100%', config: { tension: 120, friction: 14 } }), respects reduced motion with immediate: true.",
537
+ "Validation errors use `text-sm text-destructive mt-1` with `text-wrap: balance`.",
538
+ "In loading state, skeletons for input `h-10 w-full bg-muted rounded-md` and selector `h-32 w-full bg-muted rounded-md` with shimmer animation.",
539
+ "In error state, Alert uses `variant=\"destructive\"` with `border border-destructive text-destructive p-4 rounded-md`.",
540
+ "Buttons flex justify-end gap-4, hover effects wrapped in `@media (hover: hover) and (pointer: fine) { ... }` with `transition: background-color 150ms ease`.",
541
+ "Focus outlines on inputs and buttons use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with neutral colors."
542
+ ]
543
+ },
544
+ "props": [
545
+ {
546
+ "name": "name",
547
+ "type": "string",
548
+ "required": true,
549
+ "description": "Chat name value (controlled).",
550
+ "category": "data"
551
+ },
552
+ {
553
+ "name": "participants",
554
+ "type": "Array<{ id: string; name: string; avatar?: string }>",
555
+ "required": true,
556
+ "description": "Available participants.",
557
+ "category": "data"
558
+ },
559
+ {
560
+ "name": "selectedParticipants",
561
+ "type": "Array<{ id: string; name: string; avatar?: string }>",
562
+ "required": true,
563
+ "description": "Selected participants (controlled).",
564
+ "category": "data"
565
+ },
566
+ {
567
+ "name": "onNameChange",
568
+ "type": "(name: string) => void",
569
+ "required": true,
570
+ "description": "Callback on name change.",
571
+ "category": "callback"
572
+ },
573
+ {
574
+ "name": "onParticipantsChange",
575
+ "type": "(selected: Array<{ id: string; name: string; avatar?: string }>) => void",
576
+ "required": true,
577
+ "description": "Callback on participants change.",
578
+ "category": "callback"
579
+ },
580
+ {
581
+ "name": "onSubmit",
582
+ "type": "(data: { name: string; participants: Array<{ id: string; name: string; avatar?: string }> }) => void",
583
+ "required": true,
584
+ "description": "Callback to submit form.",
585
+ "category": "callback"
586
+ },
587
+ {
588
+ "name": "onCancel",
589
+ "type": "() => void",
590
+ "required": true,
591
+ "description": "Callback to cancel creation.",
592
+ "category": "callback"
593
+ },
594
+ {
595
+ "name": "isLoading",
596
+ "type": "boolean",
597
+ "required": false,
598
+ "default": "false",
599
+ "description": "Loading state.",
600
+ "category": "state"
601
+ },
602
+ {
603
+ "name": "isSubmitting",
604
+ "type": "boolean",
605
+ "required": false,
606
+ "default": "false",
607
+ "description": "Submitting state.",
608
+ "category": "state"
609
+ },
610
+ {
611
+ "name": "error",
612
+ "type": "string | null",
613
+ "required": false,
614
+ "default": "null",
615
+ "description": "Error message.",
616
+ "category": "state"
617
+ },
618
+ {
619
+ "name": "onRetry",
620
+ "type": "() => void",
621
+ "required": false,
622
+ "description": "Retry callback.",
623
+ "category": "callback"
624
+ }
625
+ ],
626
+ "storyVariants": [
627
+ {
628
+ "name": "Default",
629
+ "description": "Ready state with values.",
630
+ "args": {
631
+ "name": "New Chat",
632
+ "participants": [
633
+ {
634
+ "id": "1",
635
+ "name": "Alice"
636
+ },
637
+ {
638
+ "id": "2",
639
+ "name": "Bob"
640
+ }
641
+ ],
642
+ "selectedParticipants": [
643
+ {
644
+ "id": "1",
645
+ "name": "Alice"
646
+ }
647
+ ],
648
+ "onNameChange": "fn()",
649
+ "onParticipantsChange": "fn()",
650
+ "onSubmit": "fn()",
651
+ "onCancel": "fn()",
652
+ "isLoading": false,
653
+ "isSubmitting": false,
654
+ "error": null,
655
+ "onRetry": "fn()"
656
+ },
657
+ "needsPlayFunction": true,
658
+ "playDescription": "1. Find name input with getByLabelText('Chat name'), 2. userEvent.type(nameInput, ' Group'), 3. expect(onNameChange).toHaveBeenCalledWith('New Chat Group'), 4. Find submit button with getByRole('button', { name: 'Create Chat' }), 5. userEvent.click(submitButton), 6. expect(onSubmit).toHaveBeenCalledWith({ name: 'New Chat Group', participants: [{id: '1', name: 'Alice'}] })."
659
+ },
660
+ {
661
+ "name": "Loading",
662
+ "description": "Loading state.",
663
+ "args": {
664
+ "name": "",
665
+ "participants": [],
666
+ "selectedParticipants": [],
667
+ "onNameChange": "fn()",
668
+ "onParticipantsChange": "fn()",
669
+ "onSubmit": "fn()",
670
+ "onCancel": "fn()",
671
+ "isLoading": true,
672
+ "isSubmitting": false,
673
+ "error": null,
674
+ "onRetry": "fn()"
675
+ },
676
+ "needsPlayFunction": false
677
+ },
678
+ {
679
+ "name": "Submitting",
680
+ "description": "Submitting state.",
681
+ "args": {
682
+ "name": "New Chat",
683
+ "participants": [],
684
+ "selectedParticipants": [],
685
+ "onNameChange": "fn()",
686
+ "onParticipantsChange": "fn()",
687
+ "onSubmit": "fn()",
688
+ "onCancel": "fn()",
689
+ "isLoading": false,
690
+ "isSubmitting": true,
691
+ "error": null,
692
+ "onRetry": "fn()"
693
+ },
694
+ "needsPlayFunction": false
695
+ },
696
+ {
697
+ "name": "Error",
698
+ "description": "Error state.",
699
+ "args": {
700
+ "name": "",
701
+ "participants": [],
702
+ "selectedParticipants": [],
703
+ "onNameChange": "fn()",
704
+ "onParticipantsChange": "fn()",
705
+ "onSubmit": "fn()",
706
+ "onCancel": "fn()",
707
+ "isLoading": false,
708
+ "isSubmitting": false,
709
+ "error": "Failed to create chat",
710
+ "onRetry": "fn()"
711
+ },
712
+ "needsPlayFunction": true,
713
+ "playDescription": "1. Find retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. expect(onRetry).toHaveBeenCalled()."
714
+ },
715
+ {
716
+ "name": "Invalid",
717
+ "description": "Invalid state with errors.",
718
+ "args": {
719
+ "name": "",
720
+ "participants": [
721
+ {
722
+ "id": "1",
723
+ "name": "Alice"
724
+ }
725
+ ],
726
+ "selectedParticipants": [],
727
+ "onNameChange": "fn()",
728
+ "onParticipantsChange": "fn()",
729
+ "onSubmit": "fn()",
730
+ "onCancel": "fn()",
731
+ "isLoading": false,
732
+ "isSubmitting": false,
733
+ "error": null,
734
+ "onRetry": "fn()"
735
+ },
736
+ "needsPlayFunction": true,
737
+ "playDescription": "1. Find submit button, 2. userEvent.click(submitButton), 3. await waitFor(() => expect(getByText('Chat name is required')).toBeVisible()), 4. expect(getByText('At least one participant required')).toBeVisible()."
738
+ }
739
+ ],
740
+ "dataContract": {
741
+ "source": "local-state",
742
+ "fields": ["chatName", "selectedParticipants.id", "selectedParticipants.name"]
743
+ }
744
+ },
745
+ {
746
+ "componentId": "create-chat-page",
747
+ "componentName": "CreateChatPage",
748
+ "isNew": true,
749
+ "atomicType": "page",
750
+ "composes": ["chat-form"],
751
+ "specDeltas": {
752
+ "structure": [
753
+ "Page-level component with header region and form region using vertical stack layout",
754
+ "Header as <header> with title 'Create New Chat'",
755
+ "Form region occupies main content area composing ChatForm"
756
+ ],
757
+ "rendering": [
758
+ "In loading state, shows Skeleton in header (h-8 w-48) and form region (h-64 w-full), hides form region content",
759
+ "In ready state, shows header title and full ChatForm",
760
+ "In editing-invalid state, shows header and ChatForm with validation errors visible",
761
+ "In submitting state, shows header and ChatForm with submitting indicator, disables interactions",
762
+ "In error state, shows header and error Alert in form region with 'Failed to create chat' message and retry button, hides form fields and actions regions"
763
+ ],
764
+ "interaction": [
765
+ "On page load, fetches participant data via GraphQL query, transitions from loading to ready or error",
766
+ "On form submit from ChatForm, sends GraphQL mutation to create chat, transitions to submitting, then on success navigates to view-messages, on fail to error",
767
+ "On cancel from ChatForm, navigates to chat-list, with unsaved changes guard showing AlertDialog for confirmation",
768
+ "On retry in error state, re-fetches participants and transitions to loading"
769
+ ],
770
+ "styling": [
771
+ "Page container uses `max-w-md mx-auto p-6 flex flex-col gap-6 min-h-screen bg-background`.",
772
+ "Header uses `text-2xl font-semibold tracking-tight text-foreground text-wrap balance`.",
773
+ "Form region uses `flex-1` with ChatForm styling.",
774
+ "In loading state, header skeleton `h-8 w-48 bg-muted rounded-md`, form skeleton `h-64 w-full bg-muted rounded-md` 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`.",
775
+ "Page fade-in on load uses useSpring({ opacity: 0 to 1, config: { tension: 400, friction: 26 } }), respects useReducedMotion() with immediate: true.",
776
+ "In error state, Alert uses `variant=\"destructive\"` with `border border-destructive text-destructive p-4 rounded-md` and fade-in useTransition({ from: { opacity: 0, scale: 0.95 }, enter: { opacity: 1, scale: 1 }, config: { tension: 300, friction: 22 } }).",
777
+ "Retry button uses `variant=\"default\"` with `bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md active:scale-[0.97] transition: transform 100ms ease-out`, hover in `@media (hover: hover) and (pointer: fine)`.",
778
+ "All interactive elements have min-h-11 min-w-11 for touch targets, touch-action: manipulation.",
779
+ "Focus outlines use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` with neutral colors.",
780
+ "Z-index for error alert `z-[500]` from fixed scale for popover-like elevation.",
781
+ "Subtle separators between regions use `box-shadow: 0 0 0 1px rgba(0,0,0,0.08)` instead of border.",
782
+ "Dark mode adjusts shadows to lower opacity, borders to increased opacity for visibility."
783
+ ]
784
+ },
785
+ "props": [
786
+ {
787
+ "name": "isLoading",
788
+ "type": "boolean",
789
+ "required": false,
790
+ "default": "false",
791
+ "description": "Controls loading state for stories.",
792
+ "category": "state"
793
+ },
794
+ {
795
+ "name": "isSubmitting",
796
+ "type": "boolean",
797
+ "required": false,
798
+ "default": "false",
799
+ "description": "Controls submitting state for stories.",
800
+ "category": "state"
801
+ },
802
+ {
803
+ "name": "error",
804
+ "type": "string | null",
805
+ "required": false,
806
+ "default": "null",
807
+ "description": "Controls error state for stories.",
808
+ "category": "state"
809
+ }
810
+ ],
811
+ "storyVariants": [
812
+ {
813
+ "name": "Default",
814
+ "description": "Ready state.",
815
+ "args": {
816
+ "isLoading": false,
817
+ "isSubmitting": false,
818
+ "error": null
819
+ },
820
+ "needsPlayFunction": true,
821
+ "playDescription": "1. Find submit button with getByRole('button', { name: 'Create Chat' }), 2. userEvent.click(submitButton), 3. await waitFor(() => expect(getByText('Chat name is required')).toBeVisible())."
822
+ },
823
+ {
824
+ "name": "Loading",
825
+ "description": "Loading state.",
826
+ "args": {
827
+ "isLoading": true,
828
+ "isSubmitting": false,
829
+ "error": null
830
+ },
831
+ "needsPlayFunction": false
832
+ },
833
+ {
834
+ "name": "Submitting",
835
+ "description": "Submitting state.",
836
+ "args": {
837
+ "isLoading": false,
838
+ "isSubmitting": true,
839
+ "error": null
840
+ },
841
+ "needsPlayFunction": false
842
+ },
843
+ {
844
+ "name": "Error",
845
+ "description": "Error state.",
846
+ "args": {
847
+ "isLoading": false,
848
+ "isSubmitting": false,
849
+ "error": "Failed to create chat"
850
+ },
851
+ "needsPlayFunction": true,
852
+ "playDescription": "1. Find retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. await waitFor(() => expect(getByText('Loading...')).toBeVisible())."
853
+ }
854
+ ],
855
+ "dataContract": {
856
+ "source": "graphql-mutation",
857
+ "operationName": "CreateChat",
858
+ "fields": ["name", "participants.id"]
859
+ }
860
+ },
861
+ {
862
+ "componentId": "chat-page",
863
+ "componentName": "ChatPage",
864
+ "isNew": true,
865
+ "atomicType": "page",
866
+ "composes": ["message-list", "message-composer", "ui-components-card"],
867
+ "specDeltas": {
868
+ "structure": [
869
+ "Page-level component with chat-header region, message-list region, and message-composer region using vertical stack layout",
870
+ "Header as <header> with chat title and participants",
871
+ "MessageList in main <section> with role='log'",
872
+ "MessageComposer in <footer>"
873
+ ],
874
+ "rendering": [
875
+ "In loading state, shows Skeleton in chat-header (h-8 w-48) and message-list region (h-64 w-full), hides message-composer region",
876
+ "In ready state, shows full header, MessageList with messages, and MessageComposer",
877
+ "In error state, shows header and error Alert in message-list region with 'Failed to load messages' message and retry button, hides message-list and message-composer regions"
878
+ ],
879
+ "interaction": [
880
+ "On page load, fetches messages via GraphQL query using chatId param, transitions from loading to ready or error",
881
+ "On send from MessageComposer, sends GraphQL mutation, shows sending feedback, on success appends to messages and clears input, on fail shows send-error Alert with retry",
882
+ "On retry in error state, re-fetches messages and transitions to loading",
883
+ "Subscribes to real-time message updates via GraphQL subscription, appends new messages to list"
884
+ ],
885
+ "styling": [
886
+ "Page container uses `flex flex-col h-screen bg-background`.",
887
+ "Chat header uses `sticky top-0 z-[200] bg-background border-b p-4 flex items-center justify-between shadow-sm`.",
888
+ "Message list uses `flex-1` with MessageList styling.",
889
+ "Message composer uses `border-t p-4 bg-background`.",
890
+ "In loading state, header skeleton `h-8 w-48 bg-muted rounded-md`, list skeleton `h-full w-full bg-muted` with multiple message skeletons.",
891
+ "Page subtle fade-in uses useSpring({ opacity: 0 to 1, config: { tension: 400, friction: 26 } }), respects reduced motion.",
892
+ "In error state, Alert in list area `m-4 border border-destructive text-destructive p-4 rounded-md` with useTransition fade-in { tension: 300, friction: 22 }.",
893
+ "Retry button `variant=\"default\" bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 rounded-md active:scale-[0.97] transition: transform 100ms ease-out`, hover in media query.",
894
+ "Interactive elements min-h-11 min-w-11, touch-action: manipulation.",
895
+ "Focus outlines `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` neutral.",
896
+ "Z-index header 200 for sticky, error alert 500.",
897
+ "Separators use box-shadow ring for subtle effect.",
898
+ "Dark mode shadow reduction, border opacity increase.",
899
+ "Composer uses `env(safe-area-inset-bottom)` padding for mobile."
900
+ ]
901
+ },
902
+ "props": [
903
+ {
904
+ "name": "chatId",
905
+ "type": "string",
906
+ "required": true,
907
+ "description": "ID of the chat to display.",
908
+ "category": "config"
909
+ },
910
+ {
911
+ "name": "isLoading",
912
+ "type": "boolean",
913
+ "required": false,
914
+ "default": "false",
915
+ "description": "Controls loading state for stories.",
916
+ "category": "state"
917
+ },
918
+ {
919
+ "name": "error",
920
+ "type": "string | null",
921
+ "required": false,
922
+ "default": "null",
923
+ "description": "Controls error state for stories.",
924
+ "category": "state"
925
+ }
926
+ ],
927
+ "storyVariants": [
928
+ {
929
+ "name": "Default",
930
+ "description": "Ready state with messages.",
931
+ "args": {
932
+ "chatId": "123",
933
+ "isLoading": false,
934
+ "error": null
935
+ },
936
+ "needsPlayFunction": true,
937
+ "playDescription": "1. Find message input with getByRole('textbox'), 2. userEvent.type(input, 'Hello{enter}'), 3. await waitFor(() => expect(getByText('Sending...')).toBeVisible()), 4. await waitFor(() => expect(getByText('Hello')).toBeVisible())."
938
+ },
939
+ {
940
+ "name": "Loading",
941
+ "description": "Loading state.",
942
+ "args": {
943
+ "chatId": "123",
944
+ "isLoading": true,
945
+ "error": null
946
+ },
947
+ "needsPlayFunction": false
948
+ },
949
+ {
950
+ "name": "Error",
951
+ "description": "Error state.",
952
+ "args": {
953
+ "chatId": "123",
954
+ "isLoading": false,
955
+ "error": "Failed to load messages"
956
+ },
957
+ "needsPlayFunction": true,
958
+ "playDescription": "1. Find retry button with getByRole('button', { name: 'Retry' }), 2. userEvent.click(retryButton), 3. await waitFor(() => expect(getByText('Loading...')).toBeVisible())."
959
+ }
960
+ ],
961
+ "dataContract": {
962
+ "source": "graphql-query",
963
+ "operationName": "GetChatMessages",
964
+ "fields": ["chatId", "messages.id", "messages.sender.name", "messages.text", "messages.timestamp"]
965
+ }
966
+ }
967
+ ]