@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,150 @@
|
|
|
1
|
+
import { useRef, useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
interface PreviewPaneProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
/**
|
|
9
|
+
* If true, copy component stylesheets into the shadow root.
|
|
10
|
+
* This allows Tailwind and other CSS to work inside the preview.
|
|
11
|
+
*/
|
|
12
|
+
includeComponentStyles?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Collect CSS that should be injected into the shadow root.
|
|
17
|
+
* This includes component stylesheets and CSS custom properties.
|
|
18
|
+
*/
|
|
19
|
+
function collectComponentStyles(): string[] {
|
|
20
|
+
const styles: string[] = [];
|
|
21
|
+
|
|
22
|
+
// Collect inline styles (often used by Vite for CSS-in-JS)
|
|
23
|
+
document.querySelectorAll('style[data-vite-dev-id]').forEach((style) => {
|
|
24
|
+
if (style.textContent) {
|
|
25
|
+
styles.push(style.textContent);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Collect external stylesheets from the same origin
|
|
30
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
|
31
|
+
const href = link.getAttribute('href');
|
|
32
|
+
// Skip viewer-specific stylesheets
|
|
33
|
+
if (href && !href.includes('viewer') && !href.includes('docs')) {
|
|
34
|
+
// For linked stylesheets, we can't easily get the content
|
|
35
|
+
// Instead, we'll create a link element in the shadow root
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return styles;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* PreviewPane renders children in a Shadow DOM for CSS isolation.
|
|
45
|
+
* This prevents viewer styles from leaking into the component preview.
|
|
46
|
+
*/
|
|
47
|
+
export function PreviewPane({ children, className, style, includeComponentStyles = true }: PreviewPaneProps) {
|
|
48
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
const shadowRootRef = useRef<ShadowRoot | null>(null);
|
|
50
|
+
const reactRootRef = useRef<Root | null>(null);
|
|
51
|
+
const [mounted, setMounted] = useState(false);
|
|
52
|
+
|
|
53
|
+
// Create shadow root on mount
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!containerRef.current) return;
|
|
56
|
+
|
|
57
|
+
// Create shadow root if it doesn't exist
|
|
58
|
+
if (!shadowRootRef.current) {
|
|
59
|
+
shadowRootRef.current = containerRef.current.attachShadow({ mode: 'open' });
|
|
60
|
+
|
|
61
|
+
// Create a container div inside shadow root
|
|
62
|
+
const innerContainer = document.createElement('div');
|
|
63
|
+
innerContainer.id = 'preview-root';
|
|
64
|
+
innerContainer.setAttribute('data-preview-container', 'true');
|
|
65
|
+
innerContainer.style.cssText = 'width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 24px;';
|
|
66
|
+
shadowRootRef.current.appendChild(innerContainer);
|
|
67
|
+
|
|
68
|
+
// Add base reset styles
|
|
69
|
+
// Component library CSS variables are loaded via Vite's pipeline
|
|
70
|
+
// when the library imports its own styles (e.g., import './styles/globals.scss')
|
|
71
|
+
const baseStyle = document.createElement('style');
|
|
72
|
+
baseStyle.textContent = `
|
|
73
|
+
/* Reset and base styles */
|
|
74
|
+
*, *::before, *::after {
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
}
|
|
77
|
+
:host {
|
|
78
|
+
display: block;
|
|
79
|
+
-webkit-font-smoothing: antialiased;
|
|
80
|
+
}
|
|
81
|
+
#preview-root {
|
|
82
|
+
background-color: transparent;
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
shadowRootRef.current.appendChild(baseStyle);
|
|
86
|
+
|
|
87
|
+
// Include component styles if enabled
|
|
88
|
+
if (includeComponentStyles) {
|
|
89
|
+
const componentStyles = collectComponentStyles();
|
|
90
|
+
componentStyles.forEach((css) => {
|
|
91
|
+
const styleEl = document.createElement('style');
|
|
92
|
+
styleEl.textContent = css;
|
|
93
|
+
shadowRootRef.current!.appendChild(styleEl);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Also copy any linked stylesheets that might be from the component library
|
|
97
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach((link) => {
|
|
98
|
+
const href = link.getAttribute('href');
|
|
99
|
+
// Include component library stylesheets (exclude viewer-specific ones)
|
|
100
|
+
if (href && !href.includes('/viewer/') && !href.includes('/docs/')) {
|
|
101
|
+
const linkClone = document.createElement('link');
|
|
102
|
+
linkClone.rel = 'stylesheet';
|
|
103
|
+
linkClone.href = href;
|
|
104
|
+
shadowRootRef.current!.appendChild(linkClone);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Create React root
|
|
110
|
+
reactRootRef.current = createRoot(innerContainer);
|
|
111
|
+
setMounted(true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
// Cleanup React root on unmount
|
|
116
|
+
if (reactRootRef.current) {
|
|
117
|
+
reactRootRef.current.unmount();
|
|
118
|
+
reactRootRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, [includeComponentStyles]);
|
|
122
|
+
|
|
123
|
+
// Render children into shadow root
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (mounted && reactRootRef.current) {
|
|
126
|
+
reactRootRef.current.render(children);
|
|
127
|
+
}
|
|
128
|
+
}, [children, mounted]);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
ref={containerRef}
|
|
133
|
+
className={className}
|
|
134
|
+
style={{ minHeight: '120px', ...style }}
|
|
135
|
+
data-preview-wrapper="true"
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* SimplePreviewPane - A simpler preview without Shadow DOM isolation.
|
|
142
|
+
* Use this when full isolation isn't needed.
|
|
143
|
+
*/
|
|
144
|
+
export function SimplePreviewPane({ children, className, style }: PreviewPaneProps) {
|
|
145
|
+
return (
|
|
146
|
+
<div className={className} style={style}>
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import {
|
|
4
|
+
ZOOM_LEVELS,
|
|
5
|
+
type ZoomLevel,
|
|
6
|
+
type BackgroundOption,
|
|
7
|
+
} from '../constants/ui.js';
|
|
8
|
+
import { ZoomIcon, ChevronDownIcon } from './Icons.js';
|
|
9
|
+
|
|
10
|
+
// Re-export types for consumers
|
|
11
|
+
export type { ZoomLevel, BackgroundOption };
|
|
12
|
+
export { getBackgroundStyle } from '../constants/ui.js';
|
|
13
|
+
|
|
14
|
+
// Background options with display metadata
|
|
15
|
+
const BACKGROUND_OPTIONS_UI: { value: BackgroundOption; label: string; icon: string }[] = [
|
|
16
|
+
{ value: 'white', label: 'White', icon: '◻' },
|
|
17
|
+
{ value: 'black', label: 'Black', icon: '◼' },
|
|
18
|
+
{ value: 'checkerboard', label: 'Checkerboard', icon: '▦' },
|
|
19
|
+
{ value: 'transparent', label: 'Transparent', icon: '◇' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
interface PreviewToolbarProps {
|
|
23
|
+
zoom: ZoomLevel;
|
|
24
|
+
background: BackgroundOption;
|
|
25
|
+
onZoomChange: (zoom: ZoomLevel) => void;
|
|
26
|
+
onBackgroundChange: (bg: BackgroundOption) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function PreviewToolbar({
|
|
30
|
+
zoom,
|
|
31
|
+
background,
|
|
32
|
+
onZoomChange,
|
|
33
|
+
onBackgroundChange,
|
|
34
|
+
}: PreviewToolbarProps) {
|
|
35
|
+
const [zoomOpen, setZoomOpen] = useState(false);
|
|
36
|
+
const [bgOpen, setBgOpen] = useState(false);
|
|
37
|
+
|
|
38
|
+
// Keyboard shortcuts for zoom
|
|
39
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
40
|
+
// Don't handle if in input/textarea
|
|
41
|
+
const target = e.target as HTMLElement;
|
|
42
|
+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (e.key === '=' || e.key === '+') {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
const currentIndex = ZOOM_LEVELS.indexOf(zoom);
|
|
49
|
+
if (currentIndex < ZOOM_LEVELS.length - 1) {
|
|
50
|
+
onZoomChange(ZOOM_LEVELS[currentIndex + 1]);
|
|
51
|
+
}
|
|
52
|
+
} else if (e.key === '-') {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
const currentIndex = ZOOM_LEVELS.indexOf(zoom);
|
|
55
|
+
if (currentIndex > 0) {
|
|
56
|
+
onZoomChange(ZOOM_LEVELS[currentIndex - 1]);
|
|
57
|
+
}
|
|
58
|
+
} else if (e.key === '0') {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
onZoomChange(100);
|
|
61
|
+
}
|
|
62
|
+
}, [zoom, onZoomChange]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
66
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
67
|
+
}, [handleKeyDown]);
|
|
68
|
+
|
|
69
|
+
// Close dropdowns when clicking outside
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const handleClick = () => {
|
|
72
|
+
setZoomOpen(false);
|
|
73
|
+
setBgOpen(false);
|
|
74
|
+
};
|
|
75
|
+
if (zoomOpen || bgOpen) {
|
|
76
|
+
document.addEventListener('click', handleClick);
|
|
77
|
+
return () => document.removeEventListener('click', handleClick);
|
|
78
|
+
}
|
|
79
|
+
}, [zoomOpen, bgOpen]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex items-center gap-2">
|
|
83
|
+
{/* Zoom dropdown */}
|
|
84
|
+
<div className="relative">
|
|
85
|
+
<button
|
|
86
|
+
onClick={(e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
setZoomOpen(!zoomOpen);
|
|
89
|
+
setBgOpen(false);
|
|
90
|
+
}}
|
|
91
|
+
className={clsx(
|
|
92
|
+
'flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded',
|
|
93
|
+
'text-secondary hover:text-primary',
|
|
94
|
+
'hover:bg-[--bg-hover] transition-colors',
|
|
95
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
|
|
96
|
+
)}
|
|
97
|
+
title="Zoom level (+/-/0)"
|
|
98
|
+
>
|
|
99
|
+
<ZoomIcon className="w-3.5 h-3.5" />
|
|
100
|
+
<span>{zoom}%</span>
|
|
101
|
+
<ChevronDownIcon className="w-3 h-3" />
|
|
102
|
+
</button>
|
|
103
|
+
{zoomOpen && (
|
|
104
|
+
<div className="absolute top-full left-0 mt-1 py-1 min-w-[80px] bg-[--bg-elevated] border border-[--border] rounded-lg shadow-lg z-50">
|
|
105
|
+
{ZOOM_LEVELS.map((level) => (
|
|
106
|
+
<button
|
|
107
|
+
key={level}
|
|
108
|
+
onClick={(e) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
onZoomChange(level);
|
|
111
|
+
setZoomOpen(false);
|
|
112
|
+
}}
|
|
113
|
+
className={clsx(
|
|
114
|
+
'w-full px-3 py-1.5 text-xs text-left',
|
|
115
|
+
'hover:bg-[--bg-hover] transition-colors',
|
|
116
|
+
level === zoom ? 'text-[--color-accent] font-medium' : 'text-secondary'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
{level}%
|
|
120
|
+
</button>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Divider */}
|
|
127
|
+
<div className="w-px h-4 bg-[--border]" />
|
|
128
|
+
|
|
129
|
+
{/* Background selector */}
|
|
130
|
+
<div className="relative">
|
|
131
|
+
<button
|
|
132
|
+
onClick={(e) => {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
setBgOpen(!bgOpen);
|
|
135
|
+
setZoomOpen(false);
|
|
136
|
+
}}
|
|
137
|
+
className={clsx(
|
|
138
|
+
'flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded',
|
|
139
|
+
'text-secondary hover:text-primary',
|
|
140
|
+
'hover:bg-[--bg-hover] transition-colors',
|
|
141
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
|
|
142
|
+
)}
|
|
143
|
+
title="Background color"
|
|
144
|
+
>
|
|
145
|
+
<span className="text-sm">
|
|
146
|
+
{BACKGROUND_OPTIONS_UI.find(o => o.value === background)?.icon}
|
|
147
|
+
</span>
|
|
148
|
+
<span className="capitalize">{background}</span>
|
|
149
|
+
<ChevronDownIcon className="w-3 h-3" />
|
|
150
|
+
</button>
|
|
151
|
+
{bgOpen && (
|
|
152
|
+
<div className="absolute top-full left-0 mt-1 py-1 min-w-[120px] bg-[--bg-elevated] border border-[--border] rounded-lg shadow-lg z-50">
|
|
153
|
+
{BACKGROUND_OPTIONS_UI.map((option) => (
|
|
154
|
+
<button
|
|
155
|
+
key={option.value}
|
|
156
|
+
onClick={(e) => {
|
|
157
|
+
e.stopPropagation();
|
|
158
|
+
onBackgroundChange(option.value);
|
|
159
|
+
setBgOpen(false);
|
|
160
|
+
}}
|
|
161
|
+
className={clsx(
|
|
162
|
+
'w-full px-3 py-1.5 text-xs text-left flex items-center gap-2',
|
|
163
|
+
'hover:bg-[--bg-hover] transition-colors',
|
|
164
|
+
option.value === background ? 'text-[--color-accent] font-medium' : 'text-secondary'
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
167
|
+
<span className="text-sm">{option.icon}</span>
|
|
168
|
+
{option.label}
|
|
169
|
+
</button>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|