@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,720 @@
1
+ /**
2
+ * Interactions Panel - Storybook-style interaction testing
3
+ *
4
+ * Runs play functions from Storybook stories and displays:
5
+ * - Step-by-step progress
6
+ * - Assertion results (pass/fail)
7
+ * - Error messages with stack traces
8
+ * - Re-run capabilities
9
+ * - Step-through debugging with breakpoints
10
+ */
11
+
12
+ import { useState, useCallback, useRef, useEffect } from "react";
13
+ import clsx from "clsx";
14
+ import type { PlayFunction, PlayFunctionContext, SegmentVariant } from "../../core/index.js";
15
+ import {
16
+ PlayIcon,
17
+ CheckIcon,
18
+ XIcon,
19
+ LoadingIcon,
20
+ ChevronDownIcon,
21
+ ChevronRightIcon,
22
+ RefreshIcon,
23
+ BugIcon,
24
+ PauseIcon,
25
+ StepOverIcon,
26
+ ContinueIcon,
27
+ BreakpointIcon,
28
+ BreakpointEmptyIcon,
29
+ } from "./Icons.js";
30
+
31
+ // Step execution state
32
+ interface StepResult {
33
+ name: string;
34
+ status: "pending" | "running" | "passed" | "failed" | "paused";
35
+ error?: string;
36
+ duration?: number;
37
+ }
38
+
39
+ interface InteractionResult {
40
+ status: "idle" | "running" | "passed" | "failed" | "paused";
41
+ steps: StepResult[];
42
+ error?: string;
43
+ duration?: number;
44
+ startTime?: number;
45
+ }
46
+
47
+ // Debug state
48
+ interface DebugState {
49
+ mode: "normal" | "debug";
50
+ isPaused: boolean;
51
+ currentStepIndex: number;
52
+ breakpoints: Set<number>;
53
+ }
54
+
55
+ interface InteractionsPanelProps {
56
+ /** The current variant being displayed */
57
+ variant: SegmentVariant | null;
58
+ /** Selector for the preview container element */
59
+ previewSelector?: string;
60
+ /** Key that changes when the preview updates */
61
+ previewKey?: number;
62
+ /** Args currently being used for the preview */
63
+ currentArgs?: Record<string, unknown>;
64
+ }
65
+
66
+ export function InteractionsPanel({
67
+ variant,
68
+ previewSelector = '[data-preview-container="true"]',
69
+ previewKey = 0,
70
+ currentArgs = {},
71
+ }: InteractionsPanelProps) {
72
+ const [result, setResult] = useState<InteractionResult>({
73
+ status: "idle",
74
+ steps: [],
75
+ });
76
+ const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
77
+ const [debugState, setDebugState] = useState<DebugState>({
78
+ mode: "normal",
79
+ isPaused: false,
80
+ currentStepIndex: -1,
81
+ breakpoints: new Set(),
82
+ });
83
+ const abortControllerRef = useRef<AbortController | null>(null);
84
+ const resumeResolverRef = useRef<(() => void) | null>(null);
85
+
86
+ // Reset state when variant changes
87
+ useEffect(() => {
88
+ setResult({ status: "idle", steps: [] });
89
+ setExpandedSteps(new Set());
90
+ setDebugState((prev) => ({
91
+ ...prev,
92
+ isPaused: false,
93
+ currentStepIndex: -1,
94
+ }));
95
+ }, [variant?.name, previewKey]);
96
+
97
+ // Keyboard shortcuts
98
+ useEffect(() => {
99
+ const handleKeyDown = (e: KeyboardEvent) => {
100
+ // Only handle if this panel is focused or no specific element is focused
101
+ if (document.activeElement && document.activeElement.tagName !== 'BODY') {
102
+ const isInteractionsPanelFocused = (document.activeElement as HTMLElement).closest('[data-interactions-panel]');
103
+ if (!isInteractionsPanelFocused) return;
104
+ }
105
+
106
+ switch (e.key) {
107
+ case 'F5':
108
+ e.preventDefault();
109
+ if (debugState.mode === 'debug' && !result.status.includes('running')) {
110
+ runInteractions(true);
111
+ }
112
+ break;
113
+ case 'F8':
114
+ e.preventDefault();
115
+ if (debugState.isPaused) {
116
+ handleResume();
117
+ }
118
+ break;
119
+ case 'F9':
120
+ e.preventDefault();
121
+ if (debugState.currentStepIndex >= 0) {
122
+ toggleBreakpoint(debugState.currentStepIndex);
123
+ }
124
+ break;
125
+ case 'F10':
126
+ e.preventDefault();
127
+ if (debugState.isPaused) {
128
+ handleStepOver();
129
+ }
130
+ break;
131
+ }
132
+ };
133
+
134
+ window.addEventListener('keydown', handleKeyDown);
135
+ return () => window.removeEventListener('keydown', handleKeyDown);
136
+ }, [debugState, result.status]);
137
+
138
+ const hasPlayFunction = variant?.hasPlayFunction && variant?.play;
139
+
140
+ const toggleBreakpoint = (index: number) => {
141
+ setDebugState((prev) => {
142
+ const next = new Set(prev.breakpoints);
143
+ if (next.has(index)) {
144
+ next.delete(index);
145
+ } else {
146
+ next.add(index);
147
+ }
148
+ return { ...prev, breakpoints: next };
149
+ });
150
+ };
151
+
152
+ const toggleDebugMode = () => {
153
+ setDebugState((prev) => ({
154
+ ...prev,
155
+ mode: prev.mode === 'normal' ? 'debug' : 'normal',
156
+ }));
157
+ };
158
+
159
+ const handleResume = () => {
160
+ if (resumeResolverRef.current) {
161
+ resumeResolverRef.current();
162
+ resumeResolverRef.current = null;
163
+ }
164
+ setDebugState((prev) => ({ ...prev, isPaused: false }));
165
+ setResult((prev) => ({ ...prev, status: 'running' }));
166
+ };
167
+
168
+ const handleStepOver = () => {
169
+ // Mark current step to not pause, then resume
170
+ if (resumeResolverRef.current) {
171
+ resumeResolverRef.current();
172
+ resumeResolverRef.current = null;
173
+ }
174
+ setDebugState((prev) => ({ ...prev, isPaused: false }));
175
+ setResult((prev) => ({ ...prev, status: 'running' }));
176
+ };
177
+
178
+ // Run the play function
179
+ const runInteractions = useCallback(async (withDebugger = false) => {
180
+ if (!variant?.play) return;
181
+
182
+ // Cancel any existing run
183
+ if (abortControllerRef.current) {
184
+ abortControllerRef.current.abort();
185
+ }
186
+ abortControllerRef.current = new AbortController();
187
+
188
+ const startTime = performance.now();
189
+ const steps: StepResult[] = [];
190
+ const isDebugMode = withDebugger || debugState.mode === 'debug';
191
+ let stepIndex = 0;
192
+
193
+ setResult({
194
+ status: "running",
195
+ steps: [],
196
+ startTime,
197
+ });
198
+
199
+ setDebugState((prev) => ({
200
+ ...prev,
201
+ isPaused: false,
202
+ currentStepIndex: -1,
203
+ }));
204
+
205
+ // Find the preview container
206
+ const canvasElement = document.querySelector(previewSelector) as HTMLElement;
207
+ if (!canvasElement) {
208
+ setResult({
209
+ status: "failed",
210
+ steps: [],
211
+ error: `Preview container not found: ${previewSelector}`,
212
+ duration: performance.now() - startTime,
213
+ });
214
+ return;
215
+ }
216
+
217
+ // Create the step function that tracks execution
218
+ const step = async (name: string, fn: () => Promise<void>): Promise<void> => {
219
+ const currentIndex = stepIndex++;
220
+ const stepStartTime = performance.now();
221
+
222
+ // Check for breakpoint or debug mode pause
223
+ if (isDebugMode && (debugState.breakpoints.has(currentIndex) || currentIndex === 0)) {
224
+ // Pause at breakpoint
225
+ steps.push({ name, status: "paused" });
226
+ setResult((prev) => ({
227
+ ...prev,
228
+ status: "paused",
229
+ steps: [...steps],
230
+ }));
231
+ setDebugState((prev) => ({
232
+ ...prev,
233
+ isPaused: true,
234
+ currentStepIndex: currentIndex,
235
+ }));
236
+
237
+ // Wait for resume
238
+ await new Promise<void>((resolve) => {
239
+ resumeResolverRef.current = resolve;
240
+ });
241
+
242
+ // Update to running
243
+ steps[currentIndex] = { name, status: "running" };
244
+ setResult((prev) => ({
245
+ ...prev,
246
+ status: "running",
247
+ steps: [...steps],
248
+ }));
249
+ } else {
250
+ // Add step as running
251
+ steps.push({ name, status: "running" });
252
+ setResult((prev) => ({
253
+ ...prev,
254
+ steps: [...steps],
255
+ }));
256
+ }
257
+
258
+ setDebugState((prev) => ({
259
+ ...prev,
260
+ currentStepIndex: currentIndex,
261
+ }));
262
+
263
+ try {
264
+ await fn();
265
+ steps[currentIndex] = {
266
+ name,
267
+ status: "passed",
268
+ duration: performance.now() - stepStartTime,
269
+ };
270
+ setResult((prev) => ({
271
+ ...prev,
272
+ steps: [...steps],
273
+ }));
274
+ } catch (error) {
275
+ const errorMessage = error instanceof Error ? error.message : String(error);
276
+ steps[currentIndex] = {
277
+ name,
278
+ status: "failed",
279
+ error: errorMessage,
280
+ duration: performance.now() - stepStartTime,
281
+ };
282
+ setResult((prev) => ({
283
+ ...prev,
284
+ steps: [...steps],
285
+ }));
286
+ throw error; // Re-throw to stop execution
287
+ }
288
+
289
+ // In debug mode, pause after each step if stepping
290
+ if (isDebugMode && debugState.breakpoints.has(currentIndex + 1)) {
291
+ setResult((prev) => ({
292
+ ...prev,
293
+ status: "paused",
294
+ }));
295
+ setDebugState((prev) => ({
296
+ ...prev,
297
+ isPaused: true,
298
+ }));
299
+
300
+ await new Promise<void>((resolve) => {
301
+ resumeResolverRef.current = resolve;
302
+ });
303
+ }
304
+ };
305
+
306
+ // Create the context for the play function
307
+ const context: PlayFunctionContext = {
308
+ canvasElement,
309
+ args: currentArgs,
310
+ step,
311
+ };
312
+
313
+ try {
314
+ await variant.play(context);
315
+
316
+ // If no explicit steps were defined, the whole play function is one step
317
+ if (steps.length === 0) {
318
+ steps.push({
319
+ name: "Play function",
320
+ status: "passed",
321
+ duration: performance.now() - startTime,
322
+ });
323
+ }
324
+
325
+ setResult({
326
+ status: "passed",
327
+ steps: [...steps],
328
+ duration: performance.now() - startTime,
329
+ });
330
+
331
+ setDebugState((prev) => ({
332
+ ...prev,
333
+ isPaused: false,
334
+ currentStepIndex: -1,
335
+ }));
336
+ } catch (error) {
337
+ const errorMessage = error instanceof Error ? error.message : String(error);
338
+
339
+ // If no steps failed explicitly, mark as failed at top level
340
+ if (steps.length === 0 || !steps.some((s) => s.status === "failed")) {
341
+ steps.push({
342
+ name: "Play function",
343
+ status: "failed",
344
+ error: errorMessage,
345
+ duration: performance.now() - startTime,
346
+ });
347
+ }
348
+
349
+ setResult({
350
+ status: "failed",
351
+ steps: [...steps],
352
+ error: errorMessage,
353
+ duration: performance.now() - startTime,
354
+ });
355
+
356
+ // Auto-expand failed steps
357
+ const failedIndices = steps
358
+ .map((s, i) => (s.status === "failed" ? i : -1))
359
+ .filter((i) => i >= 0);
360
+ setExpandedSteps(new Set(failedIndices));
361
+
362
+ setDebugState((prev) => ({
363
+ ...prev,
364
+ isPaused: false,
365
+ currentStepIndex: -1,
366
+ }));
367
+ }
368
+ }, [variant, previewSelector, currentArgs, debugState.mode, debugState.breakpoints]);
369
+
370
+ const toggleStep = (index: number) => {
371
+ setExpandedSteps((prev) => {
372
+ const next = new Set(prev);
373
+ if (next.has(index)) {
374
+ next.delete(index);
375
+ } else {
376
+ next.add(index);
377
+ }
378
+ return next;
379
+ });
380
+ };
381
+
382
+ const formatDuration = (ms?: number) => {
383
+ if (ms === undefined) return "";
384
+ if (ms < 1000) return `${Math.round(ms)}ms`;
385
+ return `${(ms / 1000).toFixed(2)}s`;
386
+ };
387
+
388
+ // No play function available
389
+ if (!hasPlayFunction) {
390
+ return (
391
+ <div className="h-full flex flex-col">
392
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700">
393
+ <h3 className="font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
394
+ <PlayIcon className="w-4 h-4" />
395
+ Interactions
396
+ </h3>
397
+ </div>
398
+ <div className="flex-1 flex items-center justify-center p-8 text-center">
399
+ <div className="max-w-md">
400
+ <div className="w-12 h-12 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mx-auto mb-4">
401
+ <PlayIcon className="w-6 h-6 text-gray-400" />
402
+ </div>
403
+ <h4 className="font-medium text-gray-700 dark:text-gray-300 mb-2">
404
+ No interactions defined
405
+ </h4>
406
+ <p className="text-sm text-gray-500 dark:text-gray-400">
407
+ This variant doesn't have a play function. Add a <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded text-xs">play</code> function to your Storybook story to enable interaction testing.
408
+ </p>
409
+ <div className="mt-4 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-left">
410
+ <pre className="text-xs text-gray-600 dark:text-gray-400 overflow-x-auto">
411
+ {`export const Default = {
412
+ play: async ({ canvasElement, step }) => {
413
+ const canvas = within(canvasElement);
414
+
415
+ await step('Click the button', async () => {
416
+ await userEvent.click(
417
+ canvas.getByRole('button')
418
+ );
419
+ });
420
+
421
+ await expect(
422
+ canvas.getByText('Clicked!')
423
+ ).toBeInTheDocument();
424
+ }
425
+ };`}
426
+ </pre>
427
+ </div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ );
432
+ }
433
+
434
+ return (
435
+ <div className="h-full flex flex-col" data-interactions-panel>
436
+ {/* Header */}
437
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
438
+ <h3 className="font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
439
+ <PlayIcon className="w-4 h-4" />
440
+ Interactions
441
+ </h3>
442
+ <div className="flex items-center gap-2">
443
+ {result.duration !== undefined && (
444
+ <span className="text-xs text-gray-500 dark:text-gray-400">
445
+ {formatDuration(result.duration)}
446
+ </span>
447
+ )}
448
+
449
+ {/* Debug mode toggle */}
450
+ <button
451
+ onClick={toggleDebugMode}
452
+ className={clsx(
453
+ "p-1.5 rounded-md transition-colors",
454
+ debugState.mode === 'debug'
455
+ ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"
456
+ : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
457
+ )}
458
+ title={debugState.mode === 'debug' ? "Exit debug mode" : "Enable debug mode (F5 to run with debugger)"}
459
+ >
460
+ <BugIcon className="w-4 h-4" />
461
+ </button>
462
+
463
+ {/* Debug controls when paused */}
464
+ {debugState.isPaused && (
465
+ <>
466
+ <button
467
+ onClick={handleResume}
468
+ className="p-1.5 text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900/30 rounded-md transition-colors"
469
+ title="Continue (F8)"
470
+ >
471
+ <ContinueIcon className="w-4 h-4" />
472
+ </button>
473
+ <button
474
+ onClick={handleStepOver}
475
+ className="p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-md transition-colors"
476
+ title="Step over (F10)"
477
+ >
478
+ <StepOverIcon className="w-4 h-4" />
479
+ </button>
480
+ </>
481
+ )}
482
+
483
+ <button
484
+ onClick={() => runInteractions(debugState.mode === 'debug')}
485
+ disabled={result.status === "running"}
486
+ className={clsx(
487
+ "flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
488
+ result.status === "running" || result.status === "paused"
489
+ ? "bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed"
490
+ : "bg-blue-600 hover:bg-blue-700 text-white"
491
+ )}
492
+ >
493
+ {result.status === "running" ? (
494
+ <>
495
+ <LoadingIcon className="w-4 h-4 animate-spin" />
496
+ Running...
497
+ </>
498
+ ) : result.status === "paused" ? (
499
+ <>
500
+ <PauseIcon className="w-4 h-4" />
501
+ Paused
502
+ </>
503
+ ) : result.status === "idle" ? (
504
+ <>
505
+ <PlayIcon className="w-4 h-4" />
506
+ {debugState.mode === 'debug' ? 'Debug' : 'Run'}
507
+ </>
508
+ ) : (
509
+ <>
510
+ <RefreshIcon className="w-4 h-4" />
511
+ Rerun
512
+ </>
513
+ )}
514
+ </button>
515
+ </div>
516
+ </div>
517
+
518
+ {/* Results */}
519
+ <div className="flex-1 overflow-y-auto">
520
+ {result.status === "idle" ? (
521
+ <div className="p-8 text-center">
522
+ <p className="text-sm text-gray-500 dark:text-gray-400">
523
+ Click "Run" to execute the interaction tests
524
+ </p>
525
+ </div>
526
+ ) : (
527
+ <div className="p-4 space-y-2">
528
+ {/* Overall status */}
529
+ <div
530
+ className={clsx(
531
+ "p-3 rounded-lg flex items-center justify-between",
532
+ result.status === "running" && "bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800",
533
+ result.status === "paused" && "bg-orange-50 dark:bg-orange-950/30 border border-orange-200 dark:border-orange-800",
534
+ result.status === "passed" && "bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800",
535
+ result.status === "failed" && "bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800"
536
+ )}
537
+ >
538
+ <div className="flex items-center gap-2">
539
+ {result.status === "running" && (
540
+ <LoadingIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" />
541
+ )}
542
+ {result.status === "paused" && (
543
+ <PauseIcon className="w-5 h-5 text-orange-600 dark:text-orange-400" />
544
+ )}
545
+ {result.status === "passed" && (
546
+ <CheckIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
547
+ )}
548
+ {result.status === "failed" && (
549
+ <XIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
550
+ )}
551
+ <span
552
+ className={clsx(
553
+ "font-medium",
554
+ result.status === "running" && "text-blue-700 dark:text-blue-300",
555
+ result.status === "paused" && "text-orange-700 dark:text-orange-300",
556
+ result.status === "passed" && "text-green-700 dark:text-green-300",
557
+ result.status === "failed" && "text-red-700 dark:text-red-300"
558
+ )}
559
+ >
560
+ {result.status === "running" && "Running interactions..."}
561
+ {result.status === "paused" && "Paused at breakpoint"}
562
+ {result.status === "passed" && "All interactions passed"}
563
+ {result.status === "failed" && "Interactions failed"}
564
+ </span>
565
+ </div>
566
+ <span className="text-xs text-gray-500 dark:text-gray-400">
567
+ {result.steps.filter((s) => s.status === "passed").length}/{result.steps.length} steps
568
+ </span>
569
+ </div>
570
+
571
+ {/* Steps */}
572
+ {result.steps.length > 0 && (
573
+ <div className="mt-4 space-y-1">
574
+ <div className="flex items-center justify-between mb-2">
575
+ <h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
576
+ Steps
577
+ </h4>
578
+ {debugState.mode === 'debug' && (
579
+ <span className="text-xs text-orange-600 dark:text-orange-400">
580
+ Debug mode - click gutter to set breakpoints
581
+ </span>
582
+ )}
583
+ </div>
584
+ {result.steps.map((step, index) => (
585
+ <div
586
+ key={index}
587
+ className={clsx(
588
+ "rounded-lg border transition-colors flex",
589
+ step.status === "pending" && "bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700",
590
+ step.status === "running" && "bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800",
591
+ step.status === "paused" && "bg-orange-50 dark:bg-orange-950/30 border-orange-200 dark:border-orange-800",
592
+ step.status === "passed" && "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700",
593
+ step.status === "failed" && "bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800",
594
+ debugState.currentStepIndex === index && "ring-2 ring-orange-400 dark:ring-orange-500"
595
+ )}
596
+ >
597
+ {/* Breakpoint gutter - only show in debug mode */}
598
+ {debugState.mode === 'debug' && (
599
+ <button
600
+ onClick={(e) => {
601
+ e.stopPropagation();
602
+ toggleBreakpoint(index);
603
+ }}
604
+ className="w-6 flex-shrink-0 flex items-center justify-center border-r border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700/50 transition-colors"
605
+ title={debugState.breakpoints.has(index) ? "Remove breakpoint (F9)" : "Add breakpoint (F9)"}
606
+ >
607
+ {debugState.breakpoints.has(index) ? (
608
+ <BreakpointIcon className="w-3 h-3 text-red-500" />
609
+ ) : (
610
+ <BreakpointEmptyIcon className="w-3 h-3 text-gray-300 dark:text-gray-600 hover:text-red-400" />
611
+ )}
612
+ </button>
613
+ )}
614
+
615
+ <div className="flex-1">
616
+ <button
617
+ onClick={() => step.error && toggleStep(index)}
618
+ className={clsx(
619
+ "w-full p-3 flex items-center gap-2 text-left",
620
+ step.error && "cursor-pointer"
621
+ )}
622
+ disabled={!step.error}
623
+ >
624
+ {/* Current step indicator */}
625
+ {debugState.mode === 'debug' && debugState.currentStepIndex === index && (
626
+ <span className="text-orange-500 font-bold">▶</span>
627
+ )}
628
+
629
+ {/* Status icon */}
630
+ {step.status === "pending" && (
631
+ <div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600" />
632
+ )}
633
+ {step.status === "running" && (
634
+ <LoadingIcon className="w-4 h-4 text-blue-600 dark:text-blue-400 animate-spin" />
635
+ )}
636
+ {step.status === "paused" && (
637
+ <PauseIcon className="w-4 h-4 text-orange-600 dark:text-orange-400" />
638
+ )}
639
+ {step.status === "passed" && (
640
+ <CheckIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
641
+ )}
642
+ {step.status === "failed" && (
643
+ <XIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
644
+ )}
645
+
646
+ {/* Step name */}
647
+ <span
648
+ className={clsx(
649
+ "flex-1 text-sm",
650
+ step.status === "pending" && "text-gray-400 dark:text-gray-500",
651
+ step.status === "running" && "text-blue-700 dark:text-blue-300",
652
+ step.status === "paused" && "text-orange-700 dark:text-orange-300",
653
+ step.status === "passed" && "text-gray-700 dark:text-gray-300",
654
+ step.status === "failed" && "text-red-700 dark:text-red-300"
655
+ )}
656
+ >
657
+ {step.name}
658
+ </span>
659
+
660
+ {/* Duration */}
661
+ {step.duration !== undefined && (
662
+ <span className="text-xs text-gray-400 dark:text-gray-500">
663
+ {formatDuration(step.duration)}
664
+ </span>
665
+ )}
666
+
667
+ {/* Expand icon for errors */}
668
+ {step.error && (
669
+ expandedSteps.has(index) ? (
670
+ <ChevronDownIcon className="w-4 h-4 text-gray-400" />
671
+ ) : (
672
+ <ChevronRightIcon className="w-4 h-4 text-gray-400" />
673
+ )
674
+ )}
675
+ </button>
676
+
677
+ {/* Error details */}
678
+ {step.error && expandedSteps.has(index) && (
679
+ <div className="px-3 pb-3 pl-9">
680
+ <pre className="text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-950/50 p-2 rounded overflow-x-auto whitespace-pre-wrap">
681
+ {step.error}
682
+ </pre>
683
+ </div>
684
+ )}
685
+ </div>
686
+ </div>
687
+ ))}
688
+ </div>
689
+ )}
690
+
691
+ {/* Keyboard shortcuts help in debug mode */}
692
+ {debugState.mode === 'debug' && (
693
+ <div className="mt-4 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-xs text-gray-500 dark:text-gray-400">
694
+ <div className="font-medium mb-1">Keyboard shortcuts:</div>
695
+ <div className="grid grid-cols-2 gap-1">
696
+ <span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F5</kbd> Run with debugger</span>
697
+ <span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F8</kbd> Continue</span>
698
+ <span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F9</kbd> Toggle breakpoint</span>
699
+ <span><kbd className="px-1 bg-gray-200 dark:bg-gray-700 rounded">F10</kbd> Step over</span>
700
+ </div>
701
+ </div>
702
+ )}
703
+
704
+ {/* Top-level error (if no steps failed) */}
705
+ {result.error && !result.steps.some((s) => s.status === "failed") && (
706
+ <div className="mt-4">
707
+ <h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
708
+ Error
709
+ </h4>
710
+ <pre className="text-xs text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-950/50 p-3 rounded overflow-x-auto whitespace-pre-wrap">
711
+ {result.error}
712
+ </pre>
713
+ </div>
714
+ )}
715
+ </div>
716
+ )}
717
+ </div>
718
+ </div>
719
+ );
720
+ }