@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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- 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
|
+
}
|