@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.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /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" className="scroll-mt-24">
16
- <h2 className="text-base font-semibold text-primary mb-4">Related Components</h2>
17
- <div className="grid sm:grid-cols-2 gap-3">
18
- {relations.map((relation, index) => {
19
- const config = getRelationshipConfig(relation.relationship);
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={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
- }}
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
- 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
- )}
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
- className={clsx(
222
- "flex items-center justify-between px-4 border-[--border-subtle]",
223
- isBottom ? "h-10 border-b" : "h-10 border-b"
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 className="flex items-center gap-1 flex-1 overflow-hidden">
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 className="flex items-center gap-1">
298
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
230
299
  {/* Dock toggle button */}
231
300
  <button
232
301
  onClick={toggleDock}
233
- className="p-1 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
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
- <DockIcon dock={state.dock} className="w-4 h-4" />
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
- className="p-1 text-tertiary hover:text-primary hover:bg-[--bg-hover] rounded transition-colors"
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
- <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
- />
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, className }: { dock: "bottom" | "right"; className?: string }) {
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 className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
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 className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
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 clsx from 'clsx';
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
- <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);
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
- 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>
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
- <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>
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
  });