@fragments-sdk/cli 0.10.1 → 0.11.1

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 (149) hide show
  1. package/dist/bin.js +20 -2
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{init-NDQXUWDU.js → init-UFGK5TCN.js} +75 -4
  4. package/dist/init-UFGK5TCN.js.map +1 -0
  5. package/dist/snapshot-SV2JOFZH.js +139 -0
  6. package/dist/snapshot-SV2JOFZH.js.map +1 -0
  7. package/dist/{viewer-DNMNC5VS.js → viewer-DLLJIMCK.js} +68 -46
  8. package/dist/viewer-DLLJIMCK.js.map +1 -0
  9. package/package.json +6 -14
  10. package/src/bin.ts +30 -0
  11. package/src/commands/init.ts +76 -1
  12. package/src/commands/snapshot.ts +197 -0
  13. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  14. package/src/viewer/server.ts +37 -22
  15. package/src/viewer/vite-plugin.ts +25 -9
  16. package/dist/init-NDQXUWDU.js.map +0 -1
  17. package/dist/viewer-DNMNC5VS.js.map +0 -1
  18. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  19. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  20. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  21. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  22. package/src/viewer/assets/fragments-logo.ts +0 -4
  23. package/src/viewer/assets/fragments_logo.png +0 -0
  24. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  25. package/src/viewer/components/ActionCapture.tsx +0 -172
  26. package/src/viewer/components/ActionsPanel.tsx +0 -332
  27. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  28. package/src/viewer/components/App.tsx +0 -582
  29. package/src/viewer/components/BottomPanel.tsx +0 -288
  30. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  31. package/src/viewer/components/CodePanel.tsx +0 -118
  32. package/src/viewer/components/CommandPalette.tsx +0 -392
  33. package/src/viewer/components/ComponentDocView.tsx +0 -164
  34. package/src/viewer/components/ComponentGraph.tsx +0 -380
  35. package/src/viewer/components/ComponentHeader.tsx +0 -88
  36. package/src/viewer/components/ContractPanel.tsx +0 -241
  37. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  38. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  39. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  40. package/src/viewer/components/FragmentEditor.tsx +0 -525
  41. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  42. package/src/viewer/components/HeaderSearch.tsx +0 -24
  43. package/src/viewer/components/HealthDashboard.tsx +0 -441
  44. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  45. package/src/viewer/components/Icons.tsx +0 -479
  46. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  47. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  48. package/src/viewer/components/IsolatedRender.tsx +0 -113
  49. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  50. package/src/viewer/components/LandingPage.tsx +0 -421
  51. package/src/viewer/components/Layout.tsx +0 -27
  52. package/src/viewer/components/LeftSidebar.tsx +0 -472
  53. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  54. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  55. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  56. package/src/viewer/components/PanelShell.tsx +0 -161
  57. package/src/viewer/components/PerformancePanel.tsx +0 -304
  58. package/src/viewer/components/PreviewArea.tsx +0 -472
  59. package/src/viewer/components/PreviewAside.tsx +0 -168
  60. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  61. package/src/viewer/components/PreviewPane.tsx +0 -149
  62. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  63. package/src/viewer/components/PropsEditor.tsx +0 -506
  64. package/src/viewer/components/PropsTable.tsx +0 -111
  65. package/src/viewer/components/RelationsSection.tsx +0 -88
  66. package/src/viewer/components/ResizablePanel.tsx +0 -271
  67. package/src/viewer/components/RightSidebar.tsx +0 -102
  68. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  69. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  70. package/src/viewer/components/Sidebar.tsx +0 -169
  71. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  72. package/src/viewer/components/ThemeProvider.tsx +0 -42
  73. package/src/viewer/components/Toast.tsx +0 -3
  74. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  75. package/src/viewer/components/TopToolbar.tsx +0 -159
  76. package/src/viewer/components/UsageSection.tsx +0 -95
  77. package/src/viewer/components/VariantMatrix.tsx +0 -388
  78. package/src/viewer/components/VariantRenderer.tsx +0 -131
  79. package/src/viewer/components/VariantTabs.tsx +0 -40
  80. package/src/viewer/components/ViewerHeader.tsx +0 -69
  81. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  82. package/src/viewer/components/ViewportSelector.tsx +0 -172
  83. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  84. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  85. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  86. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  87. package/src/viewer/components/viewer-utils.ts +0 -16
  88. package/src/viewer/composition-renderer.ts +0 -381
  89. package/src/viewer/constants/index.ts +0 -1
  90. package/src/viewer/constants/ui.ts +0 -166
  91. package/src/viewer/entry.tsx +0 -335
  92. package/src/viewer/hooks/index.ts +0 -2
  93. package/src/viewer/hooks/useA11yCache.ts +0 -383
  94. package/src/viewer/hooks/useA11yService.ts +0 -364
  95. package/src/viewer/hooks/useActions.ts +0 -138
  96. package/src/viewer/hooks/useAppState.ts +0 -147
  97. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  98. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  99. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  100. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  101. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  102. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  103. package/src/viewer/hooks/useUrlState.ts +0 -318
  104. package/src/viewer/hooks/useViewSettings.ts +0 -111
  105. package/src/viewer/index.html +0 -28
  106. package/src/viewer/intelligence/healthReport.ts +0 -505
  107. package/src/viewer/intelligence/styleDrift.ts +0 -340
  108. package/src/viewer/intelligence/usageScanner.ts +0 -309
  109. package/src/viewer/jsx-parser.ts +0 -486
  110. package/src/viewer/preview-frame-entry.tsx +0 -25
  111. package/src/viewer/preview-frame.html +0 -125
  112. package/src/viewer/public/favicon.ico +0 -0
  113. package/src/viewer/render-template.html +0 -68
  114. package/src/viewer/styles/globals.css +0 -278
  115. package/src/viewer/types/a11y.ts +0 -197
  116. package/src/viewer/utils/a11y-fixes.ts +0 -509
  117. package/src/viewer/utils/actionExport.ts +0 -372
  118. package/src/viewer/utils/colorSchemes.ts +0 -201
  119. package/src/viewer/utils/detectRelationships.ts +0 -256
  120. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  121. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  122. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  123. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  124. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  125. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  126. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  127. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  128. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  129. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  130. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  131. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  132. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  133. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  134. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  135. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  136. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  137. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  138. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  139. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  140. package/src/viewer/vendor/shared/src/index.ts +0 -34
  141. package/src/viewer/vendor/shared/src/types.ts +0 -53
  142. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  143. package/src/viewer/webmcp/analytics.ts +0 -165
  144. package/src/viewer/webmcp/index.ts +0 -3
  145. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  146. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  147. package/src/viewer/webmcp/scan-utils.ts +0 -135
  148. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  149. package/src/viewer/webmcp/viewer-state.ts +0 -45
@@ -1,757 +0,0 @@
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 { Button, Stack, Text, Badge, CodeBlock, Alert, Box } from "@fragments-sdk/ui";
14
- import type { PlayFunction, PlayFunctionContext, FragmentVariant } from "../../core/index.js";
15
- import { Play } from "@phosphor-icons/react";
16
- import {
17
- PlayIcon,
18
- CheckIcon,
19
- XIcon,
20
- LoadingIcon,
21
- ChevronDownIcon,
22
- ChevronRightIcon,
23
- RefreshIcon,
24
- BugIcon,
25
- PauseIcon,
26
- StepOverIcon,
27
- ContinueIcon,
28
- BreakpointIcon,
29
- BreakpointEmptyIcon,
30
- } from "./Icons.js";
31
- import { PanelShell } from "./PanelShell.js";
32
-
33
- // Step execution state
34
- interface StepResult {
35
- name: string;
36
- status: "pending" | "running" | "passed" | "failed" | "paused";
37
- error?: string;
38
- duration?: number;
39
- }
40
-
41
- interface InteractionResult {
42
- status: "idle" | "running" | "passed" | "failed" | "paused";
43
- steps: StepResult[];
44
- error?: string;
45
- duration?: number;
46
- startTime?: number;
47
- }
48
-
49
- // Debug state
50
- interface DebugState {
51
- mode: "normal" | "debug";
52
- isPaused: boolean;
53
- currentStepIndex: number;
54
- breakpoints: Set<number>;
55
- }
56
-
57
- interface InteractionsPanelProps {
58
- /** The current variant being displayed */
59
- variant: FragmentVariant | null;
60
- /** Selector for the preview container element */
61
- previewSelector?: string;
62
- /** Key that changes when the preview updates */
63
- previewKey?: number;
64
- /** Args currently being used for the preview */
65
- currentArgs?: Record<string, unknown>;
66
- }
67
-
68
- // Status color helpers
69
- const STATUS_COLORS: Record<string, { bg: string; border: string; text: string; icon: string }> = {
70
- running: {
71
- bg: 'color-mix(in srgb, #3b82f6 8%, transparent)',
72
- border: '1px solid color-mix(in srgb, #3b82f6 25%, transparent)',
73
- text: '#1d4ed8',
74
- icon: '#2563eb',
75
- },
76
- paused: {
77
- bg: 'color-mix(in srgb, #f97316 8%, transparent)',
78
- border: '1px solid color-mix(in srgb, #f97316 25%, transparent)',
79
- text: '#c2410c',
80
- icon: '#ea580c',
81
- },
82
- passed: {
83
- bg: 'color-mix(in srgb, #22c55e 8%, transparent)',
84
- border: '1px solid color-mix(in srgb, #22c55e 25%, transparent)',
85
- text: '#15803d',
86
- icon: '#16a34a',
87
- },
88
- failed: {
89
- bg: 'color-mix(in srgb, #ef4444 8%, transparent)',
90
- border: '1px solid color-mix(in srgb, #ef4444 25%, transparent)',
91
- text: '#b91c1c',
92
- icon: '#dc2626',
93
- },
94
- pending: {
95
- bg: 'var(--bg-secondary)',
96
- border: '1px solid var(--border)',
97
- text: 'var(--text-muted)',
98
- icon: 'var(--text-muted)',
99
- },
100
- };
101
-
102
- function getStatusColors(status: string) {
103
- return STATUS_COLORS[status] || STATUS_COLORS.pending;
104
- }
105
-
106
- export function InteractionsPanel({
107
- variant,
108
- previewSelector = '[data-preview-container="true"]',
109
- previewKey = 0,
110
- currentArgs = {},
111
- }: InteractionsPanelProps) {
112
- const [result, setResult] = useState<InteractionResult>({
113
- status: "idle",
114
- steps: [],
115
- });
116
- const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
117
- const [debugState, setDebugState] = useState<DebugState>({
118
- mode: "normal",
119
- isPaused: false,
120
- currentStepIndex: -1,
121
- breakpoints: new Set(),
122
- });
123
- const [hoveredBreakpoint, setHoveredBreakpoint] = useState<number | null>(null);
124
- const abortControllerRef = useRef<AbortController | null>(null);
125
- const resumeResolverRef = useRef<(() => void) | null>(null);
126
-
127
- // Reset state when variant changes
128
- useEffect(() => {
129
- setResult({ status: "idle", steps: [] });
130
- setExpandedSteps(new Set());
131
- setDebugState((prev) => ({
132
- ...prev,
133
- isPaused: false,
134
- currentStepIndex: -1,
135
- }));
136
- }, [variant?.name, previewKey]);
137
-
138
- // Keyboard shortcuts
139
- useEffect(() => {
140
- const handleKeyDown = (e: KeyboardEvent) => {
141
- // Only handle if this panel is focused or no specific element is focused
142
- if (document.activeElement && document.activeElement.tagName !== 'BODY') {
143
- const isInteractionsPanelFocused = (document.activeElement as HTMLElement).closest('[data-interactions-panel]');
144
- if (!isInteractionsPanelFocused) return;
145
- }
146
-
147
- switch (e.key) {
148
- case 'F5':
149
- e.preventDefault();
150
- if (debugState.mode === 'debug' && !result.status.includes('running')) {
151
- runInteractions(true);
152
- }
153
- break;
154
- case 'F8':
155
- e.preventDefault();
156
- if (debugState.isPaused) {
157
- handleResume();
158
- }
159
- break;
160
- case 'F9':
161
- e.preventDefault();
162
- if (debugState.currentStepIndex >= 0) {
163
- toggleBreakpoint(debugState.currentStepIndex);
164
- }
165
- break;
166
- case 'F10':
167
- e.preventDefault();
168
- if (debugState.isPaused) {
169
- handleStepOver();
170
- }
171
- break;
172
- }
173
- };
174
-
175
- window.addEventListener('keydown', handleKeyDown);
176
- return () => window.removeEventListener('keydown', handleKeyDown);
177
- }, [debugState, result.status]);
178
-
179
- const hasPlayFunction = variant?.hasPlayFunction && variant?.play;
180
-
181
- const toggleBreakpoint = (index: number) => {
182
- setDebugState((prev) => {
183
- const next = new Set(prev.breakpoints);
184
- if (next.has(index)) {
185
- next.delete(index);
186
- } else {
187
- next.add(index);
188
- }
189
- return { ...prev, breakpoints: next };
190
- });
191
- };
192
-
193
- const toggleDebugMode = () => {
194
- setDebugState((prev) => ({
195
- ...prev,
196
- mode: prev.mode === 'normal' ? 'debug' : 'normal',
197
- }));
198
- };
199
-
200
- const handleResume = () => {
201
- if (resumeResolverRef.current) {
202
- resumeResolverRef.current();
203
- resumeResolverRef.current = null;
204
- }
205
- setDebugState((prev) => ({ ...prev, isPaused: false }));
206
- setResult((prev) => ({ ...prev, status: 'running' }));
207
- };
208
-
209
- const handleStepOver = () => {
210
- // Mark current step to not pause, then resume
211
- if (resumeResolverRef.current) {
212
- resumeResolverRef.current();
213
- resumeResolverRef.current = null;
214
- }
215
- setDebugState((prev) => ({ ...prev, isPaused: false }));
216
- setResult((prev) => ({ ...prev, status: 'running' }));
217
- };
218
-
219
- // Run the play function
220
- const runInteractions = useCallback(async (withDebugger = false) => {
221
- if (!variant?.play) return;
222
-
223
- // Cancel any existing run
224
- if (abortControllerRef.current) {
225
- abortControllerRef.current.abort();
226
- }
227
- abortControllerRef.current = new AbortController();
228
-
229
- const startTime = performance.now();
230
- const steps: StepResult[] = [];
231
- const isDebugMode = withDebugger || debugState.mode === 'debug';
232
- let stepIndex = 0;
233
-
234
- setResult({
235
- status: "running",
236
- steps: [],
237
- startTime,
238
- });
239
-
240
- setDebugState((prev) => ({
241
- ...prev,
242
- isPaused: false,
243
- currentStepIndex: -1,
244
- }));
245
-
246
- // Find the preview container
247
- const canvasElement = document.querySelector(previewSelector) as HTMLElement;
248
- if (!canvasElement) {
249
- setResult({
250
- status: "failed",
251
- steps: [],
252
- error: `Preview container not found: ${previewSelector}`,
253
- duration: performance.now() - startTime,
254
- });
255
- return;
256
- }
257
-
258
- // Create the step function that tracks execution
259
- const step = async (name: string, fn: () => Promise<void>): Promise<void> => {
260
- const currentIndex = stepIndex++;
261
- const stepStartTime = performance.now();
262
-
263
- // Check for breakpoint or debug mode pause
264
- if (isDebugMode && (debugState.breakpoints.has(currentIndex) || currentIndex === 0)) {
265
- // Pause at breakpoint
266
- steps.push({ name, status: "paused" });
267
- setResult((prev) => ({
268
- ...prev,
269
- status: "paused",
270
- steps: [...steps],
271
- }));
272
- setDebugState((prev) => ({
273
- ...prev,
274
- isPaused: true,
275
- currentStepIndex: currentIndex,
276
- }));
277
-
278
- // Wait for resume
279
- await new Promise<void>((resolve) => {
280
- resumeResolverRef.current = resolve;
281
- });
282
-
283
- // Update to running
284
- steps[currentIndex] = { name, status: "running" };
285
- setResult((prev) => ({
286
- ...prev,
287
- status: "running",
288
- steps: [...steps],
289
- }));
290
- } else {
291
- // Add step as running
292
- steps.push({ name, status: "running" });
293
- setResult((prev) => ({
294
- ...prev,
295
- steps: [...steps],
296
- }));
297
- }
298
-
299
- setDebugState((prev) => ({
300
- ...prev,
301
- currentStepIndex: currentIndex,
302
- }));
303
-
304
- try {
305
- await fn();
306
- steps[currentIndex] = {
307
- name,
308
- status: "passed",
309
- duration: performance.now() - stepStartTime,
310
- };
311
- setResult((prev) => ({
312
- ...prev,
313
- steps: [...steps],
314
- }));
315
- } catch (error) {
316
- const errorMessage = error instanceof Error ? error.message : String(error);
317
- steps[currentIndex] = {
318
- name,
319
- status: "failed",
320
- error: errorMessage,
321
- duration: performance.now() - stepStartTime,
322
- };
323
- setResult((prev) => ({
324
- ...prev,
325
- steps: [...steps],
326
- }));
327
- throw error; // Re-throw to stop execution
328
- }
329
-
330
- // In debug mode, pause after each step if stepping
331
- if (isDebugMode && debugState.breakpoints.has(currentIndex + 1)) {
332
- setResult((prev) => ({
333
- ...prev,
334
- status: "paused",
335
- }));
336
- setDebugState((prev) => ({
337
- ...prev,
338
- isPaused: true,
339
- }));
340
-
341
- await new Promise<void>((resolve) => {
342
- resumeResolverRef.current = resolve;
343
- });
344
- }
345
- };
346
-
347
- // Create the context for the play function
348
- const context: PlayFunctionContext = {
349
- canvasElement,
350
- args: currentArgs,
351
- step,
352
- };
353
-
354
- try {
355
- await variant.play(context);
356
-
357
- // If no explicit steps were defined, the whole play function is one step
358
- if (steps.length === 0) {
359
- steps.push({
360
- name: "Play function",
361
- status: "passed",
362
- duration: performance.now() - startTime,
363
- });
364
- }
365
-
366
- setResult({
367
- status: "passed",
368
- steps: [...steps],
369
- duration: performance.now() - startTime,
370
- });
371
-
372
- setDebugState((prev) => ({
373
- ...prev,
374
- isPaused: false,
375
- currentStepIndex: -1,
376
- }));
377
- } catch (error) {
378
- const errorMessage = error instanceof Error ? error.message : String(error);
379
-
380
- // If no steps failed explicitly, mark as failed at top level
381
- if (steps.length === 0 || !steps.some((s) => s.status === "failed")) {
382
- steps.push({
383
- name: "Play function",
384
- status: "failed",
385
- error: errorMessage,
386
- duration: performance.now() - startTime,
387
- });
388
- }
389
-
390
- setResult({
391
- status: "failed",
392
- steps: [...steps],
393
- error: errorMessage,
394
- duration: performance.now() - startTime,
395
- });
396
-
397
- // Auto-expand failed steps
398
- const failedIndices = steps
399
- .map((s, i) => (s.status === "failed" ? i : -1))
400
- .filter((i) => i >= 0);
401
- setExpandedSteps(new Set(failedIndices));
402
-
403
- setDebugState((prev) => ({
404
- ...prev,
405
- isPaused: false,
406
- currentStepIndex: -1,
407
- }));
408
- }
409
- }, [variant, previewSelector, currentArgs, debugState.mode, debugState.breakpoints]);
410
-
411
- const toggleStep = (index: number) => {
412
- setExpandedSteps((prev) => {
413
- const next = new Set(prev);
414
- if (next.has(index)) {
415
- next.delete(index);
416
- } else {
417
- next.add(index);
418
- }
419
- return next;
420
- });
421
- };
422
-
423
- const formatDuration = (ms?: number) => {
424
- if (ms === undefined) return "";
425
- if (ms < 1000) return `${Math.round(ms)}ms`;
426
- return `${(ms / 1000).toFixed(2)}s`;
427
- };
428
-
429
- // Empty state for no play function
430
- const emptyConfig = !hasPlayFunction ? {
431
- icon: <Play size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
432
- title: "No interactions defined",
433
- description: (
434
- <>
435
- This variant doesn't have a play function. Add a <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>play</Box> function to your Storybook story to enable interaction testing.
436
- </>
437
- ),
438
- action: (
439
- <CodeBlock language="typescript">{`export const Default = {
440
- play: async ({ canvasElement, step }) => {
441
- const canvas = within(canvasElement);
442
-
443
- await step('Click the button', async () => {
444
- await userEvent.click(
445
- canvas.getByRole('button')
446
- );
447
- });
448
-
449
- await expect(
450
- canvas.getByText('Clicked!')
451
- ).toBeInTheDocument();
452
- }
453
- };`}</CodeBlock>
454
- ),
455
- } : undefined;
456
-
457
- // Toolbar with duration, debug controls, and run button
458
- const toolbar = hasPlayFunction ? (
459
- <Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
460
- <Stack direction="row" align="center" gap="sm">
461
- {result.duration !== undefined && (
462
- <Text size="xs" color="tertiary">{formatDuration(result.duration)}</Text>
463
- )}
464
- </Stack>
465
- <Stack direction="row" align="center" gap="sm">
466
- {/* Debug mode toggle */}
467
- <Button
468
- onClick={toggleDebugMode}
469
- variant="ghost"
470
- size="sm"
471
- title={debugState.mode === 'debug' ? "Exit debug mode" : "Enable debug mode (F5 to run with debugger)"}
472
- style={{
473
- color: debugState.mode === 'debug' ? '#ea580c' : undefined,
474
- background: debugState.mode === 'debug' ? 'color-mix(in srgb, #f97316 10%, transparent)' : undefined,
475
- }}
476
- >
477
- <BugIcon style={{ width: 16, height: 16 }} />
478
- </Button>
479
-
480
- {/* Debug controls when paused */}
481
- {debugState.isPaused && (
482
- <>
483
- <Button
484
- onClick={handleResume}
485
- variant="ghost"
486
- size="sm"
487
- title="Continue (F8)"
488
- style={{ color: '#16a34a' }}
489
- >
490
- <ContinueIcon style={{ width: 16, height: 16 }} />
491
- </Button>
492
- <Button
493
- onClick={handleStepOver}
494
- variant="ghost"
495
- size="sm"
496
- title="Step over (F10)"
497
- style={{ color: '#2563eb' }}
498
- >
499
- <StepOverIcon style={{ width: 16, height: 16 }} />
500
- </Button>
501
- </>
502
- )}
503
-
504
- <Button
505
- onClick={() => runInteractions(debugState.mode === 'debug')}
506
- disabled={result.status === "running"}
507
- variant={result.status === "running" || result.status === "paused" ? "outline" : "solid"}
508
- size="sm"
509
- style={
510
- result.status === "running" || result.status === "paused"
511
- ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)', cursor: 'not-allowed' }
512
- : { background: 'var(--color-accent)', color: '#fff' }
513
- }
514
- >
515
- {result.status === "running" ? (
516
- <>
517
- <LoadingIcon style={{ width: 16, height: 16, animation: 'spin 1s linear infinite' }} />
518
- Running...
519
- </>
520
- ) : result.status === "paused" ? (
521
- <>
522
- <PauseIcon style={{ width: 16, height: 16 }} />
523
- Paused
524
- </>
525
- ) : result.status === "idle" ? (
526
- <>
527
- <PlayIcon style={{ width: 16, height: 16 }} />
528
- {debugState.mode === 'debug' ? 'Debug' : 'Run'}
529
- </>
530
- ) : (
531
- <>
532
- <RefreshIcon style={{ width: 16, height: 16 }} />
533
- Rerun
534
- </>
535
- )}
536
- </Button>
537
- </Stack>
538
- </Stack>
539
- ) : undefined;
540
-
541
- return (
542
- <div data-interactions-panel style={{ height: '100%' }}>
543
- <PanelShell toolbar={toolbar} empty={emptyConfig} bodyPadding="none">
544
- {result.status === "idle" ? (
545
- <Box padding="lg" style={{ textAlign: 'center' }}>
546
- <Text size="sm" color="tertiary">
547
- Click "Run" to execute the interaction tests
548
- </Text>
549
- </Box>
550
- ) : (
551
- <Stack direction="column" gap="sm" style={{ padding: '16px' }}>
552
- {/* Overall status */}
553
- {(() => {
554
- const colors = getStatusColors(result.status);
555
- const alertVariant = result.status === 'passed' ? 'success'
556
- : result.status === 'failed' ? 'error'
557
- : result.status === 'paused' ? 'warning'
558
- : 'info';
559
- return (
560
- <Alert variant={alertVariant}>
561
- <Alert.Body>
562
- <Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
563
- <Stack direction="row" align="center" gap="sm">
564
- {result.status === "running" && (
565
- <LoadingIcon style={{ width: 20, height: 20, animation: 'spin 1s linear infinite', color: colors.icon }} />
566
- )}
567
- {result.status === "paused" && (
568
- <PauseIcon style={{ width: 20, height: 20, color: colors.icon }} />
569
- )}
570
- {result.status === "passed" && (
571
- <CheckIcon style={{ width: 20, height: 20, color: colors.icon }} />
572
- )}
573
- {result.status === "failed" && (
574
- <XIcon style={{ width: 20, height: 20, color: colors.icon }} />
575
- )}
576
- <Text weight="medium" style={{ color: colors.text }}>
577
- {result.status === "running" && "Running interactions..."}
578
- {result.status === "paused" && "Paused at breakpoint"}
579
- {result.status === "passed" && "All interactions passed"}
580
- {result.status === "failed" && "Interactions failed"}
581
- </Text>
582
- </Stack>
583
- <Text size="xs" color="tertiary">
584
- {result.steps.filter((s) => s.status === "passed").length}/{result.steps.length} steps
585
- </Text>
586
- </Stack>
587
- </Alert.Body>
588
- </Alert>
589
- );
590
- })()}
591
-
592
- {/* Steps */}
593
- {result.steps.length > 0 && (
594
- <Stack direction="column" gap="xs" style={{ marginTop: '16px' }}>
595
- <Stack direction="row" align="center" justify="between" style={{ marginBottom: '8px' }}>
596
- <Text size="xs" weight="medium" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
597
- Steps
598
- </Text>
599
- {debugState.mode === 'debug' && (
600
- <Text size="xs" style={{ color: '#ea580c' }}>
601
- Debug mode - click gutter to set breakpoints
602
- </Text>
603
- )}
604
- </Stack>
605
- {result.steps.map((step, index) => {
606
- const stepColors = getStatusColors(step.status);
607
- return (
608
- <div
609
- key={index}
610
- style={{
611
- borderRadius: '8px',
612
- border: step.status === 'passed' ? '1px solid var(--border)' : stepColors.border,
613
- transition: 'background 0.15s',
614
- display: 'flex',
615
- background: step.status === 'passed' ? 'var(--bg-primary)' : stepColors.bg,
616
- outline: debugState.currentStepIndex === index ? '2px solid #f97316' : undefined,
617
- outlineOffset: debugState.currentStepIndex === index ? '0px' : undefined,
618
- }}
619
- >
620
- {/* Breakpoint gutter - only show in debug mode */}
621
- {debugState.mode === 'debug' && (
622
- <button
623
- onClick={(e) => {
624
- e.stopPropagation();
625
- toggleBreakpoint(index);
626
- }}
627
- style={{
628
- width: '24px',
629
- flexShrink: 0,
630
- display: 'flex',
631
- alignItems: 'center',
632
- justifyContent: 'center',
633
- border: 'none',
634
- borderRight: '1px solid var(--border)',
635
- background: hoveredBreakpoint === index ? 'var(--bg-hover)' : 'transparent',
636
- transition: 'background 0.15s',
637
- cursor: 'pointer',
638
- padding: 0,
639
- }}
640
- onMouseEnter={() => setHoveredBreakpoint(index)}
641
- onMouseLeave={() => setHoveredBreakpoint(null)}
642
- title={debugState.breakpoints.has(index) ? "Remove breakpoint (F9)" : "Add breakpoint (F9)"}
643
- >
644
- {debugState.breakpoints.has(index) ? (
645
- <BreakpointIcon style={{ width: 12, height: 12, color: '#ef4444' }} />
646
- ) : (
647
- <BreakpointEmptyIcon style={{ width: 12, height: 12, color: hoveredBreakpoint === index ? '#f87171' : 'var(--text-muted)' }} />
648
- )}
649
- </button>
650
- )}
651
-
652
- <div style={{ flex: 1 }}>
653
- <button
654
- onClick={() => step.error && toggleStep(index)}
655
- style={{
656
- width: '100%',
657
- padding: '12px',
658
- display: 'flex',
659
- alignItems: 'center',
660
- gap: '8px',
661
- textAlign: 'left',
662
- cursor: step.error ? 'pointer' : 'default',
663
- border: 'none',
664
- background: 'transparent',
665
- color: 'inherit',
666
- }}
667
- disabled={!step.error}
668
- >
669
- {/* Current step indicator */}
670
- {debugState.mode === 'debug' && debugState.currentStepIndex === index && (
671
- <span style={{ color: '#f97316', fontWeight: 700 }}>&#9654;</span>
672
- )}
673
-
674
- {/* Status icon */}
675
- {step.status === "pending" && (
676
- <div style={{ width: '16px', height: '16px', borderRadius: '9999px', border: '2px solid var(--text-muted)' }} />
677
- )}
678
- {step.status === "running" && (
679
- <LoadingIcon style={{ width: 16, height: 16, animation: 'spin 1s linear infinite', color: stepColors.icon }} />
680
- )}
681
- {step.status === "paused" && (
682
- <PauseIcon style={{ width: 16, height: 16, color: stepColors.icon }} />
683
- )}
684
- {step.status === "passed" && (
685
- <CheckIcon style={{ width: 16, height: 16, color: stepColors.icon }} />
686
- )}
687
- {step.status === "failed" && (
688
- <XIcon style={{ width: 16, height: 16, color: stepColors.icon }} />
689
- )}
690
-
691
- {/* Step name */}
692
- <Text size="sm" style={{ flex: 1, color: stepColors.text }}>
693
- {step.name}
694
- </Text>
695
-
696
- {/* Duration */}
697
- {step.duration !== undefined && (
698
- <Text size="xs" color="tertiary">
699
- {formatDuration(step.duration)}
700
- </Text>
701
- )}
702
-
703
- {/* Expand icon for errors */}
704
- {step.error && (
705
- expandedSteps.has(index) ? (
706
- <ChevronDownIcon style={{ width: 16, height: 16, color: 'var(--text-muted)' }} />
707
- ) : (
708
- <ChevronRightIcon style={{ width: 16, height: 16, color: 'var(--text-muted)' }} />
709
- )
710
- )}
711
- </button>
712
-
713
- {/* Error details */}
714
- {step.error && expandedSteps.has(index) && (
715
- <div style={{ padding: '0 12px 12px 36px' }}>
716
- <pre style={{ fontSize: '12px', color: '#dc2626', background: 'color-mix(in srgb, #ef4444 10%, transparent)', padding: '8px', borderRadius: '4px', overflowX: 'auto', whiteSpace: 'pre-wrap' }}>
717
- {step.error}
718
- </pre>
719
- </div>
720
- )}
721
- </div>
722
- </div>
723
- );
724
- })}
725
- </Stack>
726
- )}
727
-
728
- {/* Keyboard shortcuts help in debug mode */}
729
- {debugState.mode === 'debug' && (
730
- <Box padding="sm" background="secondary" rounded="lg" style={{ marginTop: '16px' }}>
731
- <Text size="xs" weight="medium" color="tertiary" style={{ marginBottom: '4px' }}>Keyboard shortcuts:</Text>
732
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
733
- <Text size="xs" color="tertiary"><Badge size="sm" variant="default">F5</Badge> Run with debugger</Text>
734
- <Text size="xs" color="tertiary"><Badge size="sm" variant="default">F8</Badge> Continue</Text>
735
- <Text size="xs" color="tertiary"><Badge size="sm" variant="default">F9</Badge> Toggle breakpoint</Text>
736
- <Text size="xs" color="tertiary"><Badge size="sm" variant="default">F10</Badge> Step over</Text>
737
- </div>
738
- </Box>
739
- )}
740
-
741
- {/* Top-level error (if no steps failed) */}
742
- {result.error && !result.steps.some((s) => s.status === "failed") && (
743
- <Box style={{ marginTop: '16px' }}>
744
- <Text size="xs" weight="medium" color="tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '8px' }}>
745
- Error
746
- </Text>
747
- <pre style={{ fontSize: '12px', color: '#dc2626', background: 'color-mix(in srgb, #ef4444 10%, transparent)', padding: '12px', borderRadius: '4px', overflowX: 'auto', whiteSpace: 'pre-wrap' }}>
748
- {result.error}
749
- </pre>
750
- </Box>
751
- )}
752
- </Stack>
753
- )}
754
- </PanelShell>
755
- </div>
756
- );
757
- }