@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,328 @@
1
+ /**
2
+ * ResizablePanel - A panel that can be resized by dragging and docked to bottom or right
3
+ *
4
+ * Features:
5
+ * - Drag-to-resize from the edge
6
+ * - Dock to bottom or right side
7
+ * - Collapsible
8
+ * - Persists size and dock position to localStorage
9
+ */
10
+
11
+ import { useState, useEffect, useCallback, useRef, type ReactNode } from "react";
12
+ import clsx from "clsx";
13
+ import { BRAND } from "../../core/index.js";
14
+ import { ChevronDownIcon } from "./Icons.js";
15
+
16
+ // Storage key for persisting panel state
17
+ const STORAGE_KEY = `${BRAND.storagePrefix}panel-state`;
18
+
19
+ interface PanelState {
20
+ height: number;
21
+ width: number;
22
+ dock: "bottom" | "right";
23
+ isOpen: boolean;
24
+ }
25
+
26
+ const DEFAULT_STATE: PanelState = {
27
+ height: 180, // Reduced from 256 to give preview area more space
28
+ width: 400,
29
+ dock: "bottom",
30
+ isOpen: true,
31
+ };
32
+
33
+ const MIN_HEIGHT = 120;
34
+ const MAX_HEIGHT = 600;
35
+ const MIN_WIDTH = 280;
36
+ const MAX_WIDTH = 800;
37
+
38
+ function loadPanelState(): PanelState {
39
+ try {
40
+ const stored = localStorage.getItem(STORAGE_KEY);
41
+ if (stored) {
42
+ const parsed = JSON.parse(stored);
43
+ return { ...DEFAULT_STATE, ...parsed };
44
+ }
45
+ } catch {
46
+ // Ignore parse errors
47
+ }
48
+ return DEFAULT_STATE;
49
+ }
50
+
51
+ function savePanelState(state: PanelState): void {
52
+ try {
53
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
54
+ } catch {
55
+ // Ignore storage errors
56
+ }
57
+ }
58
+
59
+ interface ResizablePanelProps {
60
+ /** Panel header content (tabs, title, etc.) */
61
+ header: ReactNode;
62
+ /** Panel body content */
63
+ children: ReactNode;
64
+ /** Whether to show the panel at all */
65
+ visible?: boolean;
66
+ /** Additional class name for the panel container */
67
+ className?: string;
68
+ }
69
+
70
+ export function ResizablePanel({
71
+ header,
72
+ children,
73
+ visible = true,
74
+ className,
75
+ }: ResizablePanelProps) {
76
+ const [state, setState] = useState<PanelState>(loadPanelState);
77
+ const [isResizing, setIsResizing] = useState(false);
78
+ const panelRef = useRef<HTMLDivElement>(null);
79
+ const startPosRef = useRef({ x: 0, y: 0 });
80
+ const startSizeRef = useRef({ width: 0, height: 0 });
81
+
82
+ // Save state changes to localStorage
83
+ useEffect(() => {
84
+ savePanelState(state);
85
+ }, [state]);
86
+
87
+ const handleMouseDown = useCallback(
88
+ (e: React.MouseEvent) => {
89
+ e.preventDefault();
90
+ setIsResizing(true);
91
+ startPosRef.current = { x: e.clientX, y: e.clientY };
92
+ startSizeRef.current = { width: state.width, height: state.height };
93
+ },
94
+ [state.width, state.height]
95
+ );
96
+
97
+ const handleMouseMove = useCallback(
98
+ (e: MouseEvent) => {
99
+ if (!isResizing) return;
100
+
101
+ if (state.dock === "bottom") {
102
+ // Resize height (drag up = increase, drag down = decrease)
103
+ const deltaY = startPosRef.current.y - e.clientY;
104
+ const newHeight = Math.max(
105
+ MIN_HEIGHT,
106
+ Math.min(MAX_HEIGHT, startSizeRef.current.height + deltaY)
107
+ );
108
+ setState((s) => ({ ...s, height: newHeight }));
109
+ } else {
110
+ // Resize width (drag left = increase, drag right = decrease)
111
+ const deltaX = startPosRef.current.x - e.clientX;
112
+ const newWidth = Math.max(
113
+ MIN_WIDTH,
114
+ Math.min(MAX_WIDTH, startSizeRef.current.width + deltaX)
115
+ );
116
+ setState((s) => ({ ...s, width: newWidth }));
117
+ }
118
+ },
119
+ [isResizing, state.dock]
120
+ );
121
+
122
+ const handleMouseUp = useCallback(() => {
123
+ setIsResizing(false);
124
+ }, []);
125
+
126
+ // Add global mouse event listeners during resize
127
+ useEffect(() => {
128
+ if (isResizing) {
129
+ document.addEventListener("mousemove", handleMouseMove);
130
+ document.addEventListener("mouseup", handleMouseUp);
131
+ document.body.style.cursor = state.dock === "bottom" ? "ns-resize" : "ew-resize";
132
+ document.body.style.userSelect = "none";
133
+
134
+ return () => {
135
+ document.removeEventListener("mousemove", handleMouseMove);
136
+ document.removeEventListener("mouseup", handleMouseUp);
137
+ document.body.style.cursor = "";
138
+ document.body.style.userSelect = "";
139
+ };
140
+ }
141
+ }, [isResizing, handleMouseMove, handleMouseUp, state.dock]);
142
+
143
+ const toggleOpen = useCallback(() => {
144
+ setState((s) => ({ ...s, isOpen: !s.isOpen }));
145
+ }, []);
146
+
147
+ const toggleDock = useCallback(() => {
148
+ setState((s) => ({
149
+ ...s,
150
+ dock: s.dock === "bottom" ? "right" : "bottom",
151
+ }));
152
+ }, []);
153
+
154
+ if (!visible) return null;
155
+
156
+ const isBottom = state.dock === "bottom";
157
+ const isOpen = state.isOpen;
158
+
159
+ // Header height for collapsed state
160
+ const headerHeight = 40; // h-10 = 2.5rem = 40px
161
+
162
+ return (
163
+ <div
164
+ ref={panelRef}
165
+ className={clsx(
166
+ "flex-shrink-0 bg-[--bg-secondary] relative",
167
+ isBottom
168
+ ? "border-t border-[--border]"
169
+ : "border-l border-[--border]",
170
+ className
171
+ )}
172
+ style={{
173
+ // Only transition when NOT resizing (for smooth open/close animations)
174
+ // Disable during drag to prevent sluggish feel
175
+ transition: isResizing ? 'none' : 'height 150ms ease, width 150ms ease',
176
+ ...(isBottom
177
+ ? { height: isOpen ? state.height : headerHeight }
178
+ : { width: isOpen ? state.width : headerHeight }),
179
+ // Prevent content from being selected during resize
180
+ ...(isResizing && { pointerEvents: "none" }),
181
+ }}
182
+ >
183
+ {/* Full-viewport overlay during resize - prevents iframes from capturing events */}
184
+ {isResizing && (
185
+ <div
186
+ className="fixed inset-0 z-50"
187
+ style={{
188
+ cursor: isBottom ? "ns-resize" : "ew-resize",
189
+ // Must explicitly enable pointer events since parent has pointerEvents: none
190
+ pointerEvents: "auto",
191
+ }}
192
+ />
193
+ )}
194
+
195
+ {/* Resize Handle - extends above/left of panel for easier grabbing */}
196
+ {isOpen && (
197
+ <div
198
+ className={clsx(
199
+ "absolute z-20 group",
200
+ isBottom
201
+ ? "-top-2 left-0 right-0 h-4 cursor-ns-resize"
202
+ : "top-0 -left-2 bottom-0 w-4 cursor-ew-resize",
203
+ isResizing && "bg-[--color-accent]/20"
204
+ )}
205
+ onMouseDown={handleMouseDown}
206
+ >
207
+ {/* Visual indicator - shows on hover */}
208
+ <div
209
+ className={clsx(
210
+ "absolute bg-[--color-accent] opacity-0 group-hover:opacity-50 transition-opacity",
211
+ isBottom
212
+ ? "left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-1 rounded-full"
213
+ : "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-12 rounded-full"
214
+ )}
215
+ />
216
+ </div>
217
+ )}
218
+
219
+ {/* Panel Header */}
220
+ <div
221
+ className={clsx(
222
+ "flex items-center justify-between px-4 border-[--border-subtle]",
223
+ isBottom ? "h-10 border-b" : "h-10 border-b"
224
+ )}
225
+ >
226
+ <div className="flex items-center gap-1 flex-1 overflow-hidden">
227
+ {header}
228
+ </div>
229
+ <div className="flex items-center gap-1">
230
+ {/* Dock toggle button */}
231
+ <button
232
+ onClick={toggleDock}
233
+ className="p-1 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
234
+ title={isBottom ? "Dock to right" : "Dock to bottom"}
235
+ >
236
+ <DockIcon dock={state.dock} className="w-4 h-4" />
237
+ </button>
238
+ {/* Collapse toggle button */}
239
+ <button
240
+ onClick={toggleOpen}
241
+ className="p-1 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
242
+ title={isOpen ? "Collapse panel" : "Expand panel"}
243
+ >
244
+ <ChevronDownIcon
245
+ className={clsx(
246
+ "w-4 h-4 transition-transform",
247
+ !isOpen && (isBottom ? "rotate-180" : "-rotate-90"),
248
+ isOpen && !isBottom && "rotate-90"
249
+ )}
250
+ />
251
+ </button>
252
+ </div>
253
+ </div>
254
+
255
+ {/* Panel Content */}
256
+ {isOpen && (
257
+ <div
258
+ className="overflow-auto"
259
+ style={{
260
+ height: isBottom ? `calc(100% - ${headerHeight}px)` : "100%",
261
+ width: isBottom ? "100%" : `calc(100% - ${headerHeight}px)`,
262
+ }}
263
+ >
264
+ {children}
265
+ </div>
266
+ )}
267
+ </div>
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Hook to get panel dock position for layout purposes
273
+ */
274
+ export function usePanelDock(): "bottom" | "right" {
275
+ const [dock, setDock] = useState<"bottom" | "right">(() => {
276
+ const state = loadPanelState();
277
+ return state.dock;
278
+ });
279
+
280
+ useEffect(() => {
281
+ const handleStorage = () => {
282
+ const state = loadPanelState();
283
+ setDock(state.dock);
284
+ };
285
+
286
+ // Listen for storage changes (from other tabs)
287
+ window.addEventListener("storage", handleStorage);
288
+
289
+ // Also poll for changes since localStorage events don't fire in same tab
290
+ const interval = setInterval(handleStorage, 500);
291
+
292
+ return () => {
293
+ window.removeEventListener("storage", handleStorage);
294
+ clearInterval(interval);
295
+ };
296
+ }, []);
297
+
298
+ return dock;
299
+ }
300
+
301
+ // Icon for dock position toggle
302
+ function DockIcon({ dock, className }: { dock: "bottom" | "right"; className?: string }) {
303
+ if (dock === "bottom") {
304
+ // Show icon indicating "dock to right" action
305
+ return (
306
+ <svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
307
+ {/* Outer frame */}
308
+ <rect x="1" y="2" width="14" height="12" rx="1" />
309
+ {/* Right panel indicator */}
310
+ <line x1="10" y1="2" x2="10" y2="14" />
311
+ <line x1="10" y1="8" x2="15" y2="8" strokeDasharray="2 1" />
312
+ </svg>
313
+ );
314
+ }
315
+
316
+ // Show icon indicating "dock to bottom" action
317
+ return (
318
+ <svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
319
+ {/* Outer frame */}
320
+ <rect x="1" y="2" width="14" height="12" rx="1" />
321
+ {/* Bottom panel indicator */}
322
+ <line x1="1" y1="10" x2="15" y2="10" />
323
+ <line x1="8" y1="10" x2="8" y2="14" strokeDasharray="2 1" />
324
+ </svg>
325
+ );
326
+ }
327
+
328
+ export default ResizablePanel;
@@ -0,0 +1,118 @@
1
+ import type { SegmentDefinition } from '../../core/index.js';
2
+ import { useScrollSpy } from '../hooks/useScrollSpy.js';
3
+ import clsx from 'clsx';
4
+
5
+ interface RightSidebarProps {
6
+ segment: SegmentDefinition;
7
+ }
8
+
9
+ interface TocItem {
10
+ id: string;
11
+ label: string;
12
+ children?: TocItem[];
13
+ }
14
+
15
+ export function RightSidebar({ segment }: RightSidebarProps) {
16
+ // Build table of contents from segment
17
+ const tocItems: TocItem[] = [];
18
+
19
+ // Overview section
20
+ tocItems.push({ id: 'overview', label: 'Overview' });
21
+
22
+ // Variants section with nested items
23
+ if (segment.variants && segment.variants.length > 0) {
24
+ tocItems.push({
25
+ id: 'variants',
26
+ label: 'Variants',
27
+ children: segment.variants.map((variant, index) => ({
28
+ id: `variant-${index}`,
29
+ label: variant.name,
30
+ })),
31
+ });
32
+ }
33
+
34
+ // Usage section
35
+ if (segment.usage) {
36
+ tocItems.push({ id: 'usage', label: 'Usage' });
37
+ }
38
+
39
+ // Props section
40
+ if (segment.props && Object.keys(segment.props).length > 0) {
41
+ tocItems.push({ id: 'props', label: 'Props' });
42
+ }
43
+
44
+ // Relations section
45
+ if (segment.relations && segment.relations.length > 0) {
46
+ tocItems.push({ id: 'relations', label: 'Relations' });
47
+ }
48
+
49
+ // Flatten for scroll spy
50
+ const allIds = tocItems.flatMap((item) => [
51
+ item.id,
52
+ ...(item.children?.map((child) => child.id) ?? []),
53
+ ]);
54
+
55
+ const { activeId, scrollToSection } = useScrollSpy({
56
+ sectionIds: allIds,
57
+ offset: 100,
58
+ });
59
+
60
+ return (
61
+ <div className="py-2">
62
+ <h4 className="px-3 mb-4 text-[11px] font-medium text-tertiary uppercase tracking-wider">
63
+ On this page
64
+ </h4>
65
+ <nav>
66
+ <ul className="space-y-0.5">
67
+ {tocItems.map((item) => {
68
+ const isActive = activeId === item.id;
69
+ const hasActiveChild = item.children?.some((child) => activeId === child.id);
70
+
71
+ return (
72
+ <li key={item.id}>
73
+ <button
74
+ onClick={() => scrollToSection(item.id)}
75
+ className={clsx(
76
+ 'group flex items-center w-full text-left px-3 py-1.5 text-[13px] rounded-md transition-all duration-150',
77
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent] focus-visible:ring-inset',
78
+ isActive || hasActiveChild
79
+ ? 'text-primary font-medium'
80
+ : 'text-secondary hover:text-primary hover:bg-[--bg-hover]'
81
+ )}
82
+ >
83
+ {(isActive || hasActiveChild) && (
84
+ <span className="w-0.5 h-3.5 rounded-full bg-[--color-accent] mr-2 -ml-1" />
85
+ )}
86
+ {item.label}
87
+ </button>
88
+ {item.children && item.children.length > 0 && (
89
+ <ul className="ml-4 mt-0.5 space-y-0.5 border-l border-[--border-subtle] pl-2">
90
+ {item.children.map((child) => {
91
+ const isChildActive = activeId === child.id;
92
+ return (
93
+ <li key={child.id}>
94
+ <button
95
+ onClick={() => scrollToSection(child.id)}
96
+ className={clsx(
97
+ 'block w-full text-left px-2 py-1 text-[12px] rounded-md transition-all duration-150',
98
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent] focus-visible:ring-inset',
99
+ isChildActive
100
+ ? 'text-primary font-medium'
101
+ : 'text-tertiary hover:text-secondary'
102
+ )}
103
+ >
104
+ {child.label}
105
+ </button>
106
+ </li>
107
+ );
108
+ })}
109
+ </ul>
110
+ )}
111
+ </li>
112
+ );
113
+ })}
114
+ </ul>
115
+ </nav>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ScreenshotButton component - captures screenshots of the preview area.
3
+ * Saves directly to file.
4
+ */
5
+
6
+ import { useState, memo } from 'react';
7
+ import html2canvas from 'html2canvas';
8
+ import { CameraIcon } from './Icons.js';
9
+
10
+ interface ScreenshotButtonProps {
11
+ componentName: string;
12
+ variantName: string;
13
+ }
14
+
15
+ export const ScreenshotButton = memo(function ScreenshotButton({ componentName, variantName }: ScreenshotButtonProps) {
16
+ const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
17
+
18
+ const handleSaveToFile = async () => {
19
+ setStatus('loading');
20
+ try {
21
+ const previewContainer = document.querySelector('[data-preview-container="true"]');
22
+ if (!previewContainer) throw new Error('Preview container not found');
23
+
24
+ // Wait for fonts to be fully loaded
25
+ await document.fonts.ready;
26
+
27
+ const canvas = await html2canvas(previewContainer as HTMLElement, {
28
+ backgroundColor: null,
29
+ scale: 2,
30
+ logging: false,
31
+ useCORS: true,
32
+ allowTaint: true,
33
+ // Clone callback to ensure styles are properly computed
34
+ onclone: (clonedDoc) => {
35
+ // Force the cloned document to use the same computed styles
36
+ const clonedElement = clonedDoc.querySelector('[data-preview-container="true"]');
37
+ if (clonedElement) {
38
+ // Ensure fonts are applied in the cloned document
39
+ const style = clonedDoc.createElement('style');
40
+ style.textContent = Array.from(document.styleSheets)
41
+ .map(sheet => {
42
+ try {
43
+ return Array.from(sheet.cssRules)
44
+ .map(rule => rule.cssText)
45
+ .join('\n');
46
+ } catch {
47
+ return '';
48
+ }
49
+ })
50
+ .join('\n');
51
+ clonedDoc.head.appendChild(style);
52
+ }
53
+ },
54
+ });
55
+
56
+ const blob = await new Promise<Blob>((resolve, reject) => {
57
+ canvas.toBlob((b) => {
58
+ if (b) resolve(b);
59
+ else reject(new Error('Failed to create blob'));
60
+ }, 'image/png');
61
+ });
62
+
63
+ const url = URL.createObjectURL(blob);
64
+ const a = document.createElement('a');
65
+ a.href = url;
66
+ a.download = `${componentName}-${variantName}.png`;
67
+ document.body.appendChild(a);
68
+ a.click();
69
+ document.body.removeChild(a);
70
+ URL.revokeObjectURL(url);
71
+ setStatus('success');
72
+ setTimeout(() => setStatus('idle'), 2000);
73
+ } catch (err) {
74
+ console.error('Screenshot save failed:', err);
75
+ setStatus('error');
76
+ setTimeout(() => setStatus('idle'), 2000);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <button
82
+ onClick={handleSaveToFile}
83
+ disabled={status === 'loading'}
84
+ className="p-1.5 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors disabled:opacity-50"
85
+ title="Save screenshot"
86
+ >
87
+ <CameraIcon className="w-4 h-4" />
88
+ </button>
89
+ );
90
+ });
@@ -0,0 +1,169 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import type { SegmentDefinition } from '../../core/index.js';
3
+
4
+ interface SidebarProps {
5
+ segments: Array<{ path: string; segment: SegmentDefinition }>;
6
+ activeSegment: string | null;
7
+ onSelect: (path: string) => void;
8
+ }
9
+
10
+ export function Sidebar({ segments, activeSegment, onSelect }: SidebarProps): React.ReactElement {
11
+ const [search, setSearch] = useState('');
12
+
13
+ // Group segments by category
14
+ const grouped = useMemo(() => {
15
+ const groups: Record<string, typeof segments> = {};
16
+
17
+ for (const item of segments) {
18
+ const category = item.segment.meta.category || 'uncategorized';
19
+ if (!groups[category]) {
20
+ groups[category] = [];
21
+ }
22
+ groups[category].push(item);
23
+ }
24
+
25
+ // Filter by search
26
+ if (search) {
27
+ const filtered: Record<string, typeof segments> = {};
28
+ for (const [category, items] of Object.entries(groups)) {
29
+ const matchingItems = items.filter(
30
+ (item) =>
31
+ item.segment.meta.name.toLowerCase().includes(search.toLowerCase()) ||
32
+ item.segment.meta.description.toLowerCase().includes(search.toLowerCase())
33
+ );
34
+ if (matchingItems.length > 0) {
35
+ filtered[category] = matchingItems;
36
+ }
37
+ }
38
+ return filtered;
39
+ }
40
+
41
+ return groups;
42
+ }, [segments, search]);
43
+
44
+ return (
45
+ <div
46
+ style={{
47
+ width: '280px',
48
+ height: '100vh',
49
+ background: '#f9fafb',
50
+ borderRight: '1px solid #e5e7eb',
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ overflow: 'hidden',
54
+ }}
55
+ >
56
+ {/* Header */}
57
+ <div
58
+ style={{
59
+ padding: '16px',
60
+ borderBottom: '1px solid #e5e7eb',
61
+ }}
62
+ >
63
+ <h1
64
+ style={{
65
+ margin: 0,
66
+ fontSize: '18px',
67
+ fontWeight: 700,
68
+ color: '#111827',
69
+ }}
70
+ >
71
+ Segments
72
+ </h1>
73
+ <p
74
+ style={{
75
+ margin: '4px 0 0',
76
+ fontSize: '12px',
77
+ color: '#6b7280',
78
+ }}
79
+ >
80
+ {segments.length} component{segments.length !== 1 ? 's' : ''}
81
+ </p>
82
+ </div>
83
+
84
+ {/* Search */}
85
+ <div style={{ padding: '12px 16px' }}>
86
+ <input
87
+ type="text"
88
+ placeholder="Search components..."
89
+ value={search}
90
+ onChange={(e) => setSearch(e.target.value)}
91
+ style={{
92
+ width: '100%',
93
+ padding: '8px 12px',
94
+ border: '1px solid #d1d5db',
95
+ borderRadius: '6px',
96
+ fontSize: '13px',
97
+ outline: 'none',
98
+ boxSizing: 'border-box',
99
+ }}
100
+ />
101
+ </div>
102
+
103
+ {/* Component list */}
104
+ <div
105
+ style={{
106
+ flex: 1,
107
+ overflow: 'auto',
108
+ padding: '0 8px 16px',
109
+ }}
110
+ >
111
+ {Object.entries(grouped).map(([category, items]) => (
112
+ <div key={category} style={{ marginBottom: '16px' }}>
113
+ <div
114
+ style={{
115
+ padding: '8px',
116
+ fontSize: '11px',
117
+ fontWeight: 600,
118
+ color: '#6b7280',
119
+ textTransform: 'uppercase',
120
+ letterSpacing: '0.05em',
121
+ }}
122
+ >
123
+ {category}
124
+ </div>
125
+ {items.map((item) => (
126
+ <button
127
+ key={item.path}
128
+ onClick={() => onSelect(item.path)}
129
+ style={{
130
+ display: 'block',
131
+ width: '100%',
132
+ padding: '8px 12px',
133
+ border: 'none',
134
+ borderRadius: '6px',
135
+ background: activeSegment === item.path ? '#e5e7eb' : 'transparent',
136
+ textAlign: 'left',
137
+ cursor: 'pointer',
138
+ transition: 'background 0.15s',
139
+ }}
140
+ >
141
+ <div
142
+ style={{
143
+ fontSize: '13px',
144
+ fontWeight: 500,
145
+ color: '#111827',
146
+ }}
147
+ >
148
+ {item.segment.meta.name}
149
+ </div>
150
+ <div
151
+ style={{
152
+ fontSize: '12px',
153
+ color: '#6b7280',
154
+ marginTop: '2px',
155
+ overflow: 'hidden',
156
+ textOverflow: 'ellipsis',
157
+ whiteSpace: 'nowrap',
158
+ }}
159
+ >
160
+ {item.segment.meta.description}
161
+ </div>
162
+ </button>
163
+ ))}
164
+ </div>
165
+ ))}
166
+ </div>
167
+ </div>
168
+ );
169
+ }