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