@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,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
+ }