@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,172 @@
1
+ /**
2
+ * ActionCapture component - captures DOM events for action logging.
3
+ *
4
+ * Uses event delegation to capture interactions at the DOM level,
5
+ * which works even when component render functions don't pass callbacks.
6
+ */
7
+
8
+ import { useRef, useEffect, type ReactNode } from 'react';
9
+
10
+ interface ActionCaptureProps {
11
+ children: ReactNode;
12
+ onAction: (name: string, args: unknown[]) => void;
13
+ enabled?: boolean;
14
+ }
15
+
16
+ // Events to capture
17
+ const CAPTURE_EVENTS = [
18
+ 'click',
19
+ 'dblclick',
20
+ 'change',
21
+ 'input',
22
+ 'submit',
23
+ 'focus',
24
+ 'blur',
25
+ 'keydown',
26
+ 'keyup',
27
+ ] as const;
28
+
29
+ // Map event types to action names
30
+ function getActionName(event: Event): string {
31
+ const type = event.type;
32
+ // Convert to React-style naming
33
+ switch (type) {
34
+ case 'click': return 'onClick';
35
+ case 'dblclick': return 'onDoubleClick';
36
+ case 'change': return 'onChange';
37
+ case 'input': return 'onInput';
38
+ case 'submit': return 'onSubmit';
39
+ case 'focus': return 'onFocus';
40
+ case 'blur': return 'onBlur';
41
+ case 'keydown': return 'onKeyDown';
42
+ case 'keyup': return 'onKeyUp';
43
+ default: return `on${type.charAt(0).toUpperCase() + type.slice(1)}`;
44
+ }
45
+ }
46
+
47
+ // Extract useful information about the target element
48
+ function getElementInfo(element: Element): Record<string, unknown> {
49
+ const tag = element.tagName.toLowerCase();
50
+ const info: Record<string, unknown> = { element: tag };
51
+
52
+ // Get identifying attributes
53
+ if (element.id) info.id = element.id;
54
+ if (element.className && typeof element.className === 'string') {
55
+ const classes = element.className.trim().split(/\s+/).slice(0, 3);
56
+ if (classes.length > 0 && classes[0]) info.className = classes.join(' ');
57
+ }
58
+
59
+ // Get accessibility info
60
+ const role = element.getAttribute('role');
61
+ if (role) info.role = role;
62
+
63
+ const ariaLabel = element.getAttribute('aria-label');
64
+ if (ariaLabel) info['aria-label'] = ariaLabel;
65
+
66
+ // Get text content (truncated)
67
+ const text = element.textContent?.trim();
68
+ if (text && text.length > 0 && text.length <= 50) {
69
+ info.text = text;
70
+ } else if (text && text.length > 50) {
71
+ info.text = text.slice(0, 47) + '...';
72
+ }
73
+
74
+ // Get input-specific info
75
+ if (element instanceof HTMLInputElement) {
76
+ info.type = element.type;
77
+ info.name = element.name || undefined;
78
+ if (element.type === 'checkbox' || element.type === 'radio') {
79
+ info.checked = element.checked;
80
+ } else if (element.type !== 'password') {
81
+ info.value = element.value;
82
+ }
83
+ } else if (element instanceof HTMLSelectElement) {
84
+ info.name = element.name || undefined;
85
+ info.value = element.value;
86
+ info.selectedText = element.options[element.selectedIndex]?.text;
87
+ } else if (element instanceof HTMLTextAreaElement) {
88
+ info.name = element.name || undefined;
89
+ info.value = element.value.length > 100 ? element.value.slice(0, 97) + '...' : element.value;
90
+ } else if (element instanceof HTMLButtonElement) {
91
+ info.type = element.type;
92
+ if (element.name) info.name = element.name;
93
+ }
94
+
95
+ // Clean up undefined values
96
+ return Object.fromEntries(Object.entries(info).filter(([, v]) => v !== undefined));
97
+ }
98
+
99
+ // Extract keyboard event info
100
+ function getKeyboardInfo(event: KeyboardEvent): Record<string, unknown> {
101
+ return {
102
+ key: event.key,
103
+ code: event.code,
104
+ ...(event.ctrlKey && { ctrlKey: true }),
105
+ ...(event.shiftKey && { shiftKey: true }),
106
+ ...(event.altKey && { altKey: true }),
107
+ ...(event.metaKey && { metaKey: true }),
108
+ };
109
+ }
110
+
111
+ export function ActionCapture({ children, onAction, enabled = true }: ActionCaptureProps) {
112
+ const containerRef = useRef<HTMLDivElement>(null);
113
+
114
+ useEffect(() => {
115
+ if (!enabled) return;
116
+
117
+ const container = containerRef.current;
118
+ if (!container) return;
119
+
120
+ const handleEvent = (event: Event) => {
121
+ const target = event.target;
122
+ if (!(target instanceof Element)) return;
123
+
124
+ // Skip if the event is from the container itself
125
+ if (target === container) return;
126
+
127
+ // Get action name
128
+ const actionName = getActionName(event);
129
+
130
+ // Build args array with useful info
131
+ const args: unknown[] = [];
132
+
133
+ // Add element info
134
+ const elementInfo = getElementInfo(target);
135
+ args.push(elementInfo);
136
+
137
+ // Add keyboard-specific info for key events
138
+ if (event instanceof KeyboardEvent) {
139
+ args.push(getKeyboardInfo(event));
140
+ }
141
+
142
+ // Add mouse position for click events
143
+ if (event instanceof MouseEvent && (event.type === 'click' || event.type === 'dblclick')) {
144
+ args.push({
145
+ clientX: event.clientX,
146
+ clientY: event.clientY,
147
+ button: event.button,
148
+ });
149
+ }
150
+
151
+ onAction(actionName, args);
152
+ };
153
+
154
+ // Attach event listeners with capture phase
155
+ // This ensures we catch events even if stopPropagation is called
156
+ CAPTURE_EVENTS.forEach(eventType => {
157
+ container.addEventListener(eventType, handleEvent, { capture: true });
158
+ });
159
+
160
+ return () => {
161
+ CAPTURE_EVENTS.forEach(eventType => {
162
+ container.removeEventListener(eventType, handleEvent, { capture: true });
163
+ });
164
+ };
165
+ }, [onAction, enabled]);
166
+
167
+ return (
168
+ <div ref={containerRef} className="contents">
169
+ {children}
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Actions Panel - Storybook-style event logging
3
+ *
4
+ * Displays a log of callback invocations (onClick, onChange, etc.)
5
+ * with expandable argument details and timestamps.
6
+ */
7
+
8
+ import { useState, useMemo, useRef, useEffect } from "react";
9
+ import clsx from "clsx";
10
+ import type { ActionLog } from "../hooks/useActions.js";
11
+ import { formatActionArg } from "../hooks/useActions.js";
12
+ import {
13
+ TrashIcon,
14
+ ChevronDownIcon,
15
+ ChevronRightIcon,
16
+ PlayIcon,
17
+ ExportIcon,
18
+ DownloadIcon,
19
+ CopyIcon,
20
+ CheckIcon,
21
+ } from "./Icons.js";
22
+ import {
23
+ exportActions,
24
+ copyToClipboard,
25
+ downloadAsFile,
26
+ getFileExtension,
27
+ type ExportFormat,
28
+ } from "../utils/actionExport.js";
29
+
30
+ interface ActionsPanelProps {
31
+ /** List of action logs */
32
+ logs: ActionLog[];
33
+ /** Callback to clear all logs */
34
+ onClear: () => void;
35
+ /** Component name for export context */
36
+ componentName?: string;
37
+ /** Variant name for export context */
38
+ variantName?: string;
39
+ }
40
+
41
+ export function ActionsPanel({ logs, onClear, componentName = 'Component', variantName = 'Default' }: ActionsPanelProps) {
42
+ const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
43
+ const [filter, setFilter] = useState("");
44
+ const [showExportMenu, setShowExportMenu] = useState(false);
45
+ const [copyFeedback, setCopyFeedback] = useState<string | null>(null);
46
+ const exportMenuRef = useRef<HTMLDivElement>(null);
47
+
48
+ // Close export menu when clicking outside
49
+ useEffect(() => {
50
+ const handleClickOutside = (event: MouseEvent) => {
51
+ if (exportMenuRef.current && !exportMenuRef.current.contains(event.target as Node)) {
52
+ setShowExportMenu(false);
53
+ }
54
+ };
55
+ document.addEventListener('mousedown', handleClickOutside);
56
+ return () => document.removeEventListener('mousedown', handleClickOutside);
57
+ }, []);
58
+
59
+ const filteredLogs = useMemo(() => {
60
+ if (!filter) return logs;
61
+ const lowerFilter = filter.toLowerCase();
62
+ return logs.filter((log) => log.name.toLowerCase().includes(lowerFilter));
63
+ }, [logs, filter]);
64
+
65
+ const toggleExpanded = (id: string) => {
66
+ setExpandedLogs((prev) => {
67
+ const next = new Set(prev);
68
+ if (next.has(id)) {
69
+ next.delete(id);
70
+ } else {
71
+ next.add(id);
72
+ }
73
+ return next;
74
+ });
75
+ };
76
+
77
+ const formatTime = (timestamp: number) => {
78
+ const date = new Date(timestamp);
79
+ return date.toLocaleTimeString("en-US", {
80
+ hour12: false,
81
+ hour: "2-digit",
82
+ minute: "2-digit",
83
+ second: "2-digit",
84
+ });
85
+ };
86
+
87
+ const handleExport = async (format: ExportFormat, action: 'copy' | 'download') => {
88
+ const content = exportActions(logs, format, {
89
+ componentName,
90
+ variantName,
91
+ includeComments: true,
92
+ });
93
+
94
+ if (action === 'copy') {
95
+ const success = await copyToClipboard(content);
96
+ if (success) {
97
+ setCopyFeedback(format);
98
+ setTimeout(() => setCopyFeedback(null), 2000);
99
+ }
100
+ } else {
101
+ const filename = `${componentName}-${variantName}-actions${getFileExtension(format)}`;
102
+ downloadAsFile(content, filename.toLowerCase().replace(/\s+/g, '-'));
103
+ }
104
+
105
+ setShowExportMenu(false);
106
+ };
107
+
108
+ // No logs state
109
+ if (logs.length === 0) {
110
+ return (
111
+ <div className="h-full flex flex-col">
112
+ <div className="p-4 border-b border-[--border] flex items-center justify-between">
113
+ <h3 className="font-medium text-primary flex items-center gap-2">
114
+ <PlayIcon className="w-4 h-4" />
115
+ Actions
116
+ </h3>
117
+ </div>
118
+ <div className="flex-1 flex items-center justify-center p-8 text-center">
119
+ <div className="max-w-md">
120
+ <div className="w-12 h-12 rounded-full bg-[--bg-tertiary] flex items-center justify-center mx-auto mb-4">
121
+ <PlayIcon className="w-6 h-6 text-tertiary" />
122
+ </div>
123
+ <h4 className="font-medium text-secondary mb-2">
124
+ No actions logged yet
125
+ </h4>
126
+ <p className="text-sm text-tertiary">
127
+ Interact with the component to see callback invocations here.
128
+ Actions like <code className="px-1 py-0.5 bg-[--bg-tertiary] rounded text-xs">onClick</code>, <code className="px-1 py-0.5 bg-[--bg-tertiary] rounded text-xs">onChange</code>, etc. will be logged automatically.
129
+ </p>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <div className="h-full flex flex-col">
138
+ {/* Header */}
139
+ <div className="p-3 border-b border-[--border] flex items-center justify-between gap-2">
140
+ <div className="flex items-center gap-2 flex-1">
141
+ <h3 className="font-medium text-primary flex items-center gap-2 text-sm">
142
+ <PlayIcon className="w-4 h-4" />
143
+ Actions
144
+ </h3>
145
+ <span className="text-xs text-tertiary bg-[--bg-tertiary] px-1.5 py-0.5 rounded">
146
+ {logs.length}
147
+ </span>
148
+ </div>
149
+ <div className="flex items-center gap-2">
150
+ <input
151
+ type="text"
152
+ placeholder="Filter..."
153
+ value={filter}
154
+ onChange={(e) => setFilter(e.target.value)}
155
+ className="px-2 py-1 text-xs border border-[--border] rounded bg-[--bg-elevated] text-primary w-24 focus:outline-none focus:ring-1 focus:ring-[--color-accent]"
156
+ />
157
+
158
+ {/* Export dropdown */}
159
+ <div className="relative" ref={exportMenuRef}>
160
+ <button
161
+ onClick={() => setShowExportMenu(!showExportMenu)}
162
+ className={clsx(
163
+ "p-1.5 rounded transition-colors",
164
+ showExportMenu
165
+ ? "text-[--color-accent] bg-[--bg-hover]"
166
+ : "text-tertiary hover:text-secondary hover:bg-[--bg-hover]"
167
+ )}
168
+ title="Export actions"
169
+ >
170
+ <ExportIcon className="w-4 h-4" />
171
+ </button>
172
+
173
+ {showExportMenu && (
174
+ <div className="absolute right-0 top-full mt-1 w-56 bg-[--bg-elevated] rounded-lg shadow-[--shadow-lg] border border-[--border] py-1 z-50">
175
+ <div className="px-3 py-1.5 text-xs font-medium text-tertiary uppercase tracking-wide">
176
+ Export as
177
+ </div>
178
+
179
+ {/* JSON */}
180
+ <div className="px-1">
181
+ <div className="flex items-center justify-between px-2 py-1.5 hover:bg-[--bg-hover] rounded">
182
+ <span className="text-sm text-secondary">JSON</span>
183
+ <div className="flex items-center gap-1">
184
+ <button
185
+ onClick={() => handleExport('json', 'copy')}
186
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
187
+ title="Copy to clipboard"
188
+ >
189
+ {copyFeedback === 'json' ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <CopyIcon className="w-3.5 h-3.5" />}
190
+ </button>
191
+ <button
192
+ onClick={() => handleExport('json', 'download')}
193
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
194
+ title="Download file"
195
+ >
196
+ <DownloadIcon className="w-3.5 h-3.5" />
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ {/* Jest */}
203
+ <div className="px-1">
204
+ <div className="flex items-center justify-between px-2 py-1.5 hover:bg-[--bg-hover] rounded">
205
+ <span className="text-sm text-secondary">Jest Assertions</span>
206
+ <div className="flex items-center gap-1">
207
+ <button
208
+ onClick={() => handleExport('jest', 'copy')}
209
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
210
+ title="Copy to clipboard"
211
+ >
212
+ {copyFeedback === 'jest' ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <CopyIcon className="w-3.5 h-3.5" />}
213
+ </button>
214
+ <button
215
+ onClick={() => handleExport('jest', 'download')}
216
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
217
+ title="Download file"
218
+ >
219
+ <DownloadIcon className="w-3.5 h-3.5" />
220
+ </button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+
225
+ {/* Playwright */}
226
+ <div className="px-1">
227
+ <div className="flex items-center justify-between px-2 py-1.5 hover:bg-[--bg-hover] rounded">
228
+ <span className="text-sm text-secondary">Playwright Test</span>
229
+ <div className="flex items-center gap-1">
230
+ <button
231
+ onClick={() => handleExport('playwright', 'copy')}
232
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
233
+ title="Copy to clipboard"
234
+ >
235
+ {copyFeedback === 'playwright' ? <CheckIcon className="w-3.5 h-3.5 text-green-500" /> : <CopyIcon className="w-3.5 h-3.5" />}
236
+ </button>
237
+ <button
238
+ onClick={() => handleExport('playwright', 'download')}
239
+ className="p-1 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded"
240
+ title="Download file"
241
+ >
242
+ <DownloadIcon className="w-3.5 h-3.5" />
243
+ </button>
244
+ </div>
245
+ </div>
246
+ </div>
247
+
248
+ <div className="border-t border-[--border-subtle] my-1" />
249
+ <div className="px-3 py-1.5 text-xs text-tertiary">
250
+ {logs.length} action{logs.length !== 1 ? 's' : ''} recorded
251
+ </div>
252
+ </div>
253
+ )}
254
+ </div>
255
+
256
+ <button
257
+ onClick={onClear}
258
+ className="p-1.5 text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded transition-colors"
259
+ title="Clear all actions"
260
+ >
261
+ <TrashIcon className="w-4 h-4" />
262
+ </button>
263
+ </div>
264
+ </div>
265
+
266
+ {/* Log list */}
267
+ <div className="flex-1 overflow-y-auto">
268
+ {filteredLogs.length === 0 ? (
269
+ <div className="p-4 text-center text-sm text-tertiary">
270
+ No actions match "{filter}"
271
+ </div>
272
+ ) : (
273
+ <div className="divide-y divide-[--border-subtle]">
274
+ {filteredLogs.map((log) => (
275
+ <ActionLogItem
276
+ key={log.id}
277
+ log={log}
278
+ isExpanded={expandedLogs.has(log.id)}
279
+ onToggle={() => toggleExpanded(log.id)}
280
+ formatTime={formatTime}
281
+ />
282
+ ))}
283
+ </div>
284
+ )}
285
+ </div>
286
+ </div>
287
+ );
288
+ }
289
+
290
+ interface ActionLogItemProps {
291
+ log: ActionLog;
292
+ isExpanded: boolean;
293
+ onToggle: () => void;
294
+ formatTime: (timestamp: number) => string;
295
+ }
296
+
297
+ function ActionLogItem({ log, isExpanded, onToggle, formatTime }: ActionLogItemProps) {
298
+ const hasArgs = log.args.length > 0;
299
+ const argsPreview = hasArgs
300
+ ? log.args.map((arg) => formatActionArg(arg, 50)).join(", ")
301
+ : "";
302
+
303
+ return (
304
+ <div className="hover:bg-[--bg-secondary] transition-colors">
305
+ <button
306
+ onClick={onToggle}
307
+ disabled={!hasArgs}
308
+ className={clsx(
309
+ "w-full px-3 py-2 flex items-start gap-2 text-left",
310
+ hasArgs && "cursor-pointer",
311
+ !hasArgs && "cursor-default"
312
+ )}
313
+ >
314
+ {/* Expand icon */}
315
+ <div className="flex-shrink-0 w-4 h-4 mt-0.5">
316
+ {hasArgs ? (
317
+ isExpanded ? (
318
+ <ChevronDownIcon className="w-4 h-4 text-tertiary" />
319
+ ) : (
320
+ <ChevronRightIcon className="w-4 h-4 text-tertiary" />
321
+ )
322
+ ) : (
323
+ <div className="w-4 h-4" />
324
+ )}
325
+ </div>
326
+
327
+ {/* Action name */}
328
+ <div className="flex-1 min-w-0">
329
+ <div className="flex items-center gap-2">
330
+ <span className="font-mono text-sm text-[--color-accent]">
331
+ {log.name}
332
+ </span>
333
+ {log.count > 1 && (
334
+ <span className="text-xs text-tertiary bg-[--bg-tertiary] px-1.5 py-0.5 rounded-full">
335
+ ×{log.count}
336
+ </span>
337
+ )}
338
+ </div>
339
+ {!isExpanded && hasArgs && (
340
+ <div className="text-xs text-tertiary truncate mt-0.5 font-mono">
341
+ ({argsPreview})
342
+ </div>
343
+ )}
344
+ </div>
345
+
346
+ {/* Timestamp */}
347
+ <span className="flex-shrink-0 text-xs text-tertiary font-mono">
348
+ {formatTime(log.timestamp)}
349
+ </span>
350
+ </button>
351
+
352
+ {/* Expanded args */}
353
+ {isExpanded && hasArgs && (
354
+ <div className="px-3 pb-3 pl-9">
355
+ <div className="bg-[--bg-secondary] rounded-lg p-2 overflow-x-auto">
356
+ {log.args.map((arg, index) => (
357
+ <div key={index} className="mb-1 last:mb-0">
358
+ <span className="text-xs text-tertiary">
359
+ {log.args.length > 1 ? `[${index}] ` : ""}
360
+ </span>
361
+ <pre className="inline text-xs font-mono text-secondary whitespace-pre-wrap">
362
+ {formatActionArg(arg, 500)}
363
+ </pre>
364
+ </div>
365
+ ))}
366
+ </div>
367
+ </div>
368
+ )}
369
+ </div>
370
+ );
371
+ }