@fragments-sdk/cli 0.5.2 → 0.7.0
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/dist/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import type { ComponentRelation } from '../../core/index.js';
|
|
2
|
-
import clsx from 'clsx';
|
|
3
3
|
import { getRelationshipConfig } from '../constants/ui.js';
|
|
4
4
|
import { ChevronRightIcon } from './Icons.js';
|
|
5
5
|
|
|
@@ -12,46 +12,77 @@ export function RelationsSection({ relations, onNavigate }: RelationsSectionProp
|
|
|
12
12
|
if (relations.length === 0) return null;
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<section id="relations"
|
|
16
|
-
<h2
|
|
17
|
-
<div
|
|
18
|
-
{relations.map((relation, index) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<button
|
|
23
|
-
key={index}
|
|
24
|
-
onClick={() => onNavigate?.(relation.component)}
|
|
25
|
-
className={clsx(
|
|
26
|
-
'group text-left p-4 rounded-xl border border-[--border]',
|
|
27
|
-
'bg-[--bg-elevated] hover:border-[--border-strong]',
|
|
28
|
-
'transition-all duration-150',
|
|
29
|
-
'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
|
|
30
|
-
)}
|
|
31
|
-
>
|
|
32
|
-
<div className="flex items-center gap-2 mb-1.5">
|
|
33
|
-
<span className="text-[13px] font-medium text-primary group-hover:text-[--color-accent] transition-colors">
|
|
34
|
-
{relation.component}
|
|
35
|
-
</span>
|
|
36
|
-
<span className={clsx(
|
|
37
|
-
'text-[10px] font-medium px-1.5 py-0.5 rounded',
|
|
38
|
-
config.bg,
|
|
39
|
-
config.text
|
|
40
|
-
)}>
|
|
41
|
-
{config.label}
|
|
42
|
-
</span>
|
|
43
|
-
</div>
|
|
44
|
-
{relation.note && (
|
|
45
|
-
<p className="text-[13px] text-secondary leading-relaxed">{relation.note}</p>
|
|
46
|
-
)}
|
|
47
|
-
<div className="mt-2 flex items-center text-[12px] text-tertiary group-hover:text-secondary transition-colors">
|
|
48
|
-
<span>View component</span>
|
|
49
|
-
<ChevronRightIcon className="w-3 h-3 ml-1 group-hover:translate-x-0.5 transition-transform" />
|
|
50
|
-
</div>
|
|
51
|
-
</button>
|
|
52
|
-
);
|
|
53
|
-
})}
|
|
15
|
+
<section id="relations" style={{ scrollMarginTop: '96px' }}>
|
|
16
|
+
<h2 style={{ fontSize: '16px', fontWeight: 600, color: 'var(--text-primary)', marginBottom: '16px' }}>Related Components</h2>
|
|
17
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '12px' }}>
|
|
18
|
+
{relations.map((relation, index) => (
|
|
19
|
+
<RelationButton key={index} relation={relation} onNavigate={onNavigate} />
|
|
20
|
+
))}
|
|
54
21
|
</div>
|
|
55
22
|
</section>
|
|
56
23
|
);
|
|
57
24
|
}
|
|
25
|
+
|
|
26
|
+
function RelationButton({ relation, onNavigate }: { relation: ComponentRelation; onNavigate?: (componentName: string) => void }) {
|
|
27
|
+
const config = getRelationshipConfig(relation.relationship);
|
|
28
|
+
const [hovered, setHovered] = useState(false);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => onNavigate?.(relation.component)}
|
|
33
|
+
onMouseEnter={() => setHovered(true)}
|
|
34
|
+
onMouseLeave={() => setHovered(false)}
|
|
35
|
+
style={{
|
|
36
|
+
textAlign: 'left',
|
|
37
|
+
padding: '16px',
|
|
38
|
+
borderRadius: '12px',
|
|
39
|
+
border: `1px solid ${hovered ? 'var(--border-strong)' : 'var(--border)'}`,
|
|
40
|
+
background: 'var(--bg-elevated)',
|
|
41
|
+
transition: 'all 150ms',
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
outline: 'none',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
|
47
|
+
<span style={{
|
|
48
|
+
fontSize: '13px',
|
|
49
|
+
fontWeight: 500,
|
|
50
|
+
color: hovered ? 'var(--color-accent)' : 'var(--text-primary)',
|
|
51
|
+
transition: 'color 150ms',
|
|
52
|
+
}}>
|
|
53
|
+
{relation.component}
|
|
54
|
+
</span>
|
|
55
|
+
<span style={{
|
|
56
|
+
fontSize: '10px',
|
|
57
|
+
fontWeight: 500,
|
|
58
|
+
padding: '2px 6px',
|
|
59
|
+
borderRadius: '4px',
|
|
60
|
+
background: config.bgColor ?? 'var(--bg-tertiary)',
|
|
61
|
+
color: config.textColor ?? 'var(--text-secondary)',
|
|
62
|
+
}}>
|
|
63
|
+
{config.label}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
{relation.note && (
|
|
67
|
+
<p style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: 1.6, margin: 0 }}>{relation.note}</p>
|
|
68
|
+
)}
|
|
69
|
+
<div style={{
|
|
70
|
+
marginTop: '8px',
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
fontSize: '12px',
|
|
74
|
+
color: hovered ? 'var(--text-secondary)' : 'var(--text-tertiary)',
|
|
75
|
+
transition: 'color 150ms',
|
|
76
|
+
}}>
|
|
77
|
+
<span>View component</span>
|
|
78
|
+
<ChevronRightIcon style={{
|
|
79
|
+
width: '12px',
|
|
80
|
+
height: '12px',
|
|
81
|
+
marginLeft: '4px',
|
|
82
|
+
transform: hovered ? 'translateX(2px)' : 'translateX(0)',
|
|
83
|
+
transition: 'transform 150ms',
|
|
84
|
+
}} />
|
|
85
|
+
</div>
|
|
86
|
+
</button>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* - Persists size and dock position to localStorage
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, useEffect, useCallback, useRef, type ReactNode } from "react";
|
|
12
|
-
import clsx from "clsx";
|
|
11
|
+
import { useState, useEffect, useCallback, useRef, type ReactNode, type CSSProperties } from "react";
|
|
13
12
|
import { BRAND } from "../../core/index.js";
|
|
14
13
|
import { ChevronDownIcon } from "./Icons.js";
|
|
15
14
|
|
|
@@ -159,32 +158,97 @@ export function ResizablePanel({
|
|
|
159
158
|
// Header height for collapsed state
|
|
160
159
|
const headerHeight = 40; // h-10 = 2.5rem = 40px
|
|
161
160
|
|
|
161
|
+
// Compute container styles
|
|
162
|
+
const containerStyle: CSSProperties = {
|
|
163
|
+
flexShrink: 0,
|
|
164
|
+
backgroundColor: 'var(--bg-secondary)',
|
|
165
|
+
position: 'relative',
|
|
166
|
+
...(isBottom
|
|
167
|
+
? { borderTop: '1px solid var(--border)' }
|
|
168
|
+
: { borderLeft: '1px solid var(--border)' }),
|
|
169
|
+
// Only transition when NOT resizing (for smooth open/close animations)
|
|
170
|
+
// Disable during drag to prevent sluggish feel
|
|
171
|
+
transition: isResizing ? 'none' : 'height 150ms ease, width 150ms ease',
|
|
172
|
+
...(isBottom
|
|
173
|
+
? { height: isOpen ? state.height : headerHeight }
|
|
174
|
+
: { width: isOpen ? state.width : headerHeight }),
|
|
175
|
+
// Prevent content from being selected during resize
|
|
176
|
+
...(isResizing && { pointerEvents: "none" as const }),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Resize handle styles
|
|
180
|
+
const resizeHandleStyle: CSSProperties = isBottom
|
|
181
|
+
? {
|
|
182
|
+
position: 'absolute',
|
|
183
|
+
zIndex: 20,
|
|
184
|
+
top: '-8px',
|
|
185
|
+
left: 0,
|
|
186
|
+
right: 0,
|
|
187
|
+
height: '16px',
|
|
188
|
+
cursor: 'ns-resize',
|
|
189
|
+
...(isResizing && { backgroundColor: 'rgba(var(--color-accent-rgb, 59, 130, 246), 0.2)' }),
|
|
190
|
+
}
|
|
191
|
+
: {
|
|
192
|
+
position: 'absolute',
|
|
193
|
+
zIndex: 20,
|
|
194
|
+
top: 0,
|
|
195
|
+
left: '-8px',
|
|
196
|
+
bottom: 0,
|
|
197
|
+
width: '16px',
|
|
198
|
+
cursor: 'ew-resize',
|
|
199
|
+
...(isResizing && { backgroundColor: 'rgba(var(--color-accent-rgb, 59, 130, 246), 0.2)' }),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Visual indicator styles
|
|
203
|
+
const indicatorStyle: CSSProperties = isBottom
|
|
204
|
+
? {
|
|
205
|
+
position: 'absolute',
|
|
206
|
+
backgroundColor: 'var(--color-accent)',
|
|
207
|
+
opacity: 0,
|
|
208
|
+
left: '50%',
|
|
209
|
+
top: '50%',
|
|
210
|
+
transform: 'translate(-50%, -50%)',
|
|
211
|
+
width: '48px',
|
|
212
|
+
height: '4px',
|
|
213
|
+
borderRadius: '9999px',
|
|
214
|
+
transition: 'opacity 150ms ease',
|
|
215
|
+
}
|
|
216
|
+
: {
|
|
217
|
+
position: 'absolute',
|
|
218
|
+
backgroundColor: 'var(--color-accent)',
|
|
219
|
+
opacity: 0,
|
|
220
|
+
top: '50%',
|
|
221
|
+
left: '50%',
|
|
222
|
+
transform: 'translate(-50%, -50%)',
|
|
223
|
+
width: '4px',
|
|
224
|
+
height: '48px',
|
|
225
|
+
borderRadius: '9999px',
|
|
226
|
+
transition: 'opacity 150ms ease',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Chevron icon rotation
|
|
230
|
+
const chevronStyle: CSSProperties = {
|
|
231
|
+
width: '16px',
|
|
232
|
+
height: '16px',
|
|
233
|
+
transition: 'transform 150ms ease',
|
|
234
|
+
...(!isOpen && isBottom && { transform: 'rotate(180deg)' }),
|
|
235
|
+
...(!isOpen && !isBottom && { transform: 'rotate(-90deg)' }),
|
|
236
|
+
...(isOpen && !isBottom && { transform: 'rotate(90deg)' }),
|
|
237
|
+
};
|
|
238
|
+
|
|
162
239
|
return (
|
|
163
240
|
<div
|
|
164
241
|
ref={panelRef}
|
|
165
|
-
className={
|
|
166
|
-
|
|
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
|
-
}}
|
|
242
|
+
className={className}
|
|
243
|
+
style={containerStyle}
|
|
182
244
|
>
|
|
183
245
|
{/* Full-viewport overlay during resize - prevents iframes from capturing events */}
|
|
184
246
|
{isResizing && (
|
|
185
247
|
<div
|
|
186
|
-
className="fixed inset-0 z-50"
|
|
187
248
|
style={{
|
|
249
|
+
position: 'fixed',
|
|
250
|
+
inset: 0,
|
|
251
|
+
zIndex: 50,
|
|
188
252
|
cursor: isBottom ? "ns-resize" : "ew-resize",
|
|
189
253
|
// Must explicitly enable pointer events since parent has pointerEvents: none
|
|
190
254
|
pointerEvents: "auto",
|
|
@@ -195,59 +259,96 @@ export function ResizablePanel({
|
|
|
195
259
|
{/* Resize Handle - extends above/left of panel for easier grabbing */}
|
|
196
260
|
{isOpen && (
|
|
197
261
|
<div
|
|
198
|
-
|
|
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
|
-
)}
|
|
262
|
+
style={resizeHandleStyle}
|
|
205
263
|
onMouseDown={handleMouseDown}
|
|
264
|
+
onMouseEnter={(e) => {
|
|
265
|
+
const indicator = e.currentTarget.querySelector('[data-resize-indicator]') as HTMLElement;
|
|
266
|
+
if (indicator) indicator.style.opacity = '0.5';
|
|
267
|
+
}}
|
|
268
|
+
onMouseLeave={(e) => {
|
|
269
|
+
const indicator = e.currentTarget.querySelector('[data-resize-indicator]') as HTMLElement;
|
|
270
|
+
if (indicator) indicator.style.opacity = '0';
|
|
271
|
+
}}
|
|
206
272
|
>
|
|
207
273
|
{/* 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
|
-
/>
|
|
274
|
+
<div data-resize-indicator style={indicatorStyle} />
|
|
216
275
|
</div>
|
|
217
276
|
)}
|
|
218
277
|
|
|
219
278
|
{/* Panel Header */}
|
|
220
279
|
<div
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
280
|
+
style={{
|
|
281
|
+
display: 'flex',
|
|
282
|
+
alignItems: 'center',
|
|
283
|
+
justifyContent: 'space-between',
|
|
284
|
+
padding: '0 16px',
|
|
285
|
+
height: '40px',
|
|
286
|
+
borderBottom: '1px solid var(--border-subtle)',
|
|
287
|
+
}}
|
|
225
288
|
>
|
|
226
|
-
<div
|
|
289
|
+
<div style={{
|
|
290
|
+
display: 'flex',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
gap: '4px',
|
|
293
|
+
flex: 1,
|
|
294
|
+
overflow: 'hidden',
|
|
295
|
+
}}>
|
|
227
296
|
{header}
|
|
228
297
|
</div>
|
|
229
|
-
<div
|
|
298
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
230
299
|
{/* Dock toggle button */}
|
|
231
300
|
<button
|
|
232
301
|
onClick={toggleDock}
|
|
233
|
-
|
|
302
|
+
style={{
|
|
303
|
+
padding: '4px',
|
|
304
|
+
color: 'var(--text-tertiary)',
|
|
305
|
+
backgroundColor: 'transparent',
|
|
306
|
+
border: 'none',
|
|
307
|
+
borderRadius: '4px',
|
|
308
|
+
cursor: 'pointer',
|
|
309
|
+
display: 'inline-flex',
|
|
310
|
+
transition: 'color 150ms ease, background-color 150ms ease',
|
|
311
|
+
}}
|
|
312
|
+
onMouseEnter={(e) => {
|
|
313
|
+
e.currentTarget.style.color = 'var(--text-primary)';
|
|
314
|
+
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
315
|
+
}}
|
|
316
|
+
onMouseLeave={(e) => {
|
|
317
|
+
e.currentTarget.style.color = 'var(--text-tertiary)';
|
|
318
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
319
|
+
}}
|
|
234
320
|
title={isBottom ? "Dock to right" : "Dock to bottom"}
|
|
235
321
|
>
|
|
236
|
-
<
|
|
322
|
+
<span style={{ display: 'inline-flex', width: '16px', height: '16px' }}>
|
|
323
|
+
<DockIcon dock={state.dock} />
|
|
324
|
+
</span>
|
|
237
325
|
</button>
|
|
238
326
|
{/* Collapse toggle button */}
|
|
239
327
|
<button
|
|
240
328
|
onClick={toggleOpen}
|
|
241
|
-
|
|
329
|
+
style={{
|
|
330
|
+
padding: '4px',
|
|
331
|
+
color: 'var(--text-tertiary)',
|
|
332
|
+
backgroundColor: 'transparent',
|
|
333
|
+
border: 'none',
|
|
334
|
+
borderRadius: '4px',
|
|
335
|
+
cursor: 'pointer',
|
|
336
|
+
display: 'inline-flex',
|
|
337
|
+
transition: 'color 150ms ease, background-color 150ms ease',
|
|
338
|
+
}}
|
|
339
|
+
onMouseEnter={(e) => {
|
|
340
|
+
e.currentTarget.style.color = 'var(--text-primary)';
|
|
341
|
+
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
|
|
342
|
+
}}
|
|
343
|
+
onMouseLeave={(e) => {
|
|
344
|
+
e.currentTarget.style.color = 'var(--text-tertiary)';
|
|
345
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
346
|
+
}}
|
|
242
347
|
title={isOpen ? "Collapse panel" : "Expand panel"}
|
|
243
348
|
>
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
!isOpen && (isBottom ? "rotate-180" : "-rotate-90"),
|
|
248
|
-
isOpen && !isBottom && "rotate-90"
|
|
249
|
-
)}
|
|
250
|
-
/>
|
|
349
|
+
<span style={chevronStyle}>
|
|
350
|
+
<ChevronDownIcon />
|
|
351
|
+
</span>
|
|
251
352
|
</button>
|
|
252
353
|
</div>
|
|
253
354
|
</div>
|
|
@@ -255,8 +356,8 @@ export function ResizablePanel({
|
|
|
255
356
|
{/* Panel Content */}
|
|
256
357
|
{isOpen && (
|
|
257
358
|
<div
|
|
258
|
-
className="overflow-auto"
|
|
259
359
|
style={{
|
|
360
|
+
overflow: 'auto',
|
|
260
361
|
height: isBottom ? `calc(100% - ${headerHeight}px)` : "100%",
|
|
261
362
|
width: isBottom ? "100%" : `calc(100% - ${headerHeight}px)`,
|
|
262
363
|
}}
|
|
@@ -299,11 +400,13 @@ export function usePanelDock(): "bottom" | "right" {
|
|
|
299
400
|
}
|
|
300
401
|
|
|
301
402
|
// Icon for dock position toggle
|
|
302
|
-
function DockIcon({ dock
|
|
403
|
+
function DockIcon({ dock }: { dock: "bottom" | "right" }) {
|
|
404
|
+
const svgStyle: CSSProperties = { width: '100%', height: '100%' };
|
|
405
|
+
|
|
303
406
|
if (dock === "bottom") {
|
|
304
407
|
// Show icon indicating "dock to right" action
|
|
305
408
|
return (
|
|
306
|
-
<svg
|
|
409
|
+
<svg style={svgStyle} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
307
410
|
{/* Outer frame */}
|
|
308
411
|
<rect x="1" y="2" width="14" height="12" rx="1" />
|
|
309
412
|
{/* Right panel indicator */}
|
|
@@ -315,7 +418,7 @@ function DockIcon({ dock, className }: { dock: "bottom" | "right"; className?: s
|
|
|
315
418
|
|
|
316
419
|
// Show icon indicating "dock to bottom" action
|
|
317
420
|
return (
|
|
318
|
-
<svg
|
|
421
|
+
<svg style={svgStyle} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
319
422
|
{/* Outer frame */}
|
|
320
423
|
<rect x="1" y="2" width="14" height="12" rx="1" />
|
|
321
424
|
{/* Bottom panel indicator */}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SegmentDefinition } from '../../core/index.js';
|
|
2
2
|
import { useScrollSpy } from '../hooks/useScrollSpy.js';
|
|
3
|
-
import
|
|
3
|
+
import { Sidebar } from '@fragments/ui';
|
|
4
4
|
|
|
5
5
|
interface RightSidebarProps {
|
|
6
6
|
segment: SegmentDefinition;
|
|
@@ -58,61 +58,51 @@ export function RightSidebar({ segment }: RightSidebarProps) {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
<Sidebar position="right" collapsible="none" style={{ padding: '8px 0' }}>
|
|
62
|
+
<Sidebar.Header>
|
|
63
|
+
<span style={{
|
|
64
|
+
fontSize: '11px',
|
|
65
|
+
fontWeight: 500,
|
|
66
|
+
color: 'var(--text-tertiary)',
|
|
67
|
+
textTransform: 'uppercase',
|
|
68
|
+
letterSpacing: '0.05em',
|
|
69
|
+
}}>
|
|
70
|
+
On this page
|
|
71
|
+
</span>
|
|
72
|
+
</Sidebar.Header>
|
|
73
|
+
<Sidebar.Nav aria-label="On this page">
|
|
74
|
+
{tocItems.map((item) => {
|
|
75
|
+
const isActive = activeId === item.id;
|
|
76
|
+
const hasActiveChild = item.children?.some((child) => activeId === child.id);
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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>
|
|
78
|
+
return (
|
|
79
|
+
<Sidebar.Section key={item.id}>
|
|
80
|
+
<Sidebar.Item
|
|
81
|
+
active={isActive || !!hasActiveChild}
|
|
82
|
+
onClick={() => scrollToSection(item.id)}
|
|
83
|
+
>
|
|
84
|
+
{item.label}
|
|
85
|
+
</Sidebar.Item>
|
|
86
|
+
{item.children && item.children.length > 0 && (
|
|
87
|
+
<>
|
|
88
|
+
{item.children.map((child) => {
|
|
89
|
+
const isChildActive = activeId === child.id;
|
|
90
|
+
return (
|
|
91
|
+
<Sidebar.SubItem
|
|
92
|
+
key={child.id}
|
|
93
|
+
active={isChildActive}
|
|
94
|
+
onClick={() => scrollToSection(child.id)}
|
|
95
|
+
>
|
|
96
|
+
{child.label}
|
|
97
|
+
</Sidebar.SubItem>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
</Sidebar.Section>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</Sidebar.Nav>
|
|
106
|
+
</Sidebar>
|
|
117
107
|
);
|
|
118
108
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useState, memo } from 'react';
|
|
7
7
|
import html2canvas from 'html2canvas';
|
|
8
|
+
import { Button, Tooltip } from '@fragments/ui';
|
|
8
9
|
import { CameraIcon } from './Icons.js';
|
|
9
10
|
|
|
10
11
|
interface ScreenshotButtonProps {
|
|
@@ -21,7 +22,6 @@ export const ScreenshotButton = memo(function ScreenshotButton({ componentName,
|
|
|
21
22
|
const previewContainer = document.querySelector('[data-preview-container="true"]');
|
|
22
23
|
if (!previewContainer) throw new Error('Preview container not found');
|
|
23
24
|
|
|
24
|
-
// Wait for fonts to be fully loaded
|
|
25
25
|
await document.fonts.ready;
|
|
26
26
|
|
|
27
27
|
const canvas = await html2canvas(previewContainer as HTMLElement, {
|
|
@@ -30,12 +30,9 @@ export const ScreenshotButton = memo(function ScreenshotButton({ componentName,
|
|
|
30
30
|
logging: false,
|
|
31
31
|
useCORS: true,
|
|
32
32
|
allowTaint: true,
|
|
33
|
-
// Clone callback to ensure styles are properly computed
|
|
34
33
|
onclone: (clonedDoc) => {
|
|
35
|
-
// Force the cloned document to use the same computed styles
|
|
36
34
|
const clonedElement = clonedDoc.querySelector('[data-preview-container="true"]');
|
|
37
35
|
if (clonedElement) {
|
|
38
|
-
// Ensure fonts are applied in the cloned document
|
|
39
36
|
const style = clonedDoc.createElement('style');
|
|
40
37
|
style.textContent = Array.from(document.styleSheets)
|
|
41
38
|
.map(sheet => {
|
|
@@ -78,13 +75,16 @@ export const ScreenshotButton = memo(function ScreenshotButton({ componentName,
|
|
|
78
75
|
};
|
|
79
76
|
|
|
80
77
|
return (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
<Tooltip content="Save screenshot">
|
|
79
|
+
<Button
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="sm"
|
|
82
|
+
icon
|
|
83
|
+
disabled={status === 'loading'}
|
|
84
|
+
onClick={handleSaveToFile}
|
|
85
|
+
>
|
|
86
|
+
<CameraIcon style={{ width: '16px', height: '16px' }} />
|
|
87
|
+
</Button>
|
|
88
|
+
</Tooltip>
|
|
89
89
|
);
|
|
90
90
|
});
|