@fragments-sdk/cli 0.8.1 → 0.9.1

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 (128) hide show
  1. package/dist/bin.js +517 -77
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +2 -2
  36. package/src/bin.ts +26 -0
  37. package/src/build.ts +12 -2
  38. package/src/commands/build.ts +16 -2
  39. package/src/commands/doctor.ts +498 -0
  40. package/src/commands/generate.ts +383 -68
  41. package/src/commands/init-framework.ts +1 -1
  42. package/src/commands/init.ts +9 -51
  43. package/src/core/config.ts +15 -2
  44. package/src/core/generators/typescript-extractor.ts +10 -0
  45. package/src/core/index.ts +15 -0
  46. package/src/core/schema.ts +10 -2
  47. package/src/core/storyFilters.test.ts +350 -0
  48. package/src/core/storyFilters.ts +253 -0
  49. package/src/core/types.ts +22 -0
  50. package/src/migrate/converter.ts +9 -1
  51. package/src/migrate/parser.ts +2 -0
  52. package/src/migrate/types.ts +2 -0
  53. package/src/setup.ts +69 -24
  54. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  55. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  56. package/src/viewer/components/ActionsPanel.tsx +31 -29
  57. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  58. package/src/viewer/components/App.tsx +187 -740
  59. package/src/viewer/components/BottomPanel.tsx +228 -132
  60. package/src/viewer/components/CodePanel.tsx +1 -1
  61. package/src/viewer/components/CommandPalette.tsx +7 -10
  62. package/src/viewer/components/ComponentDocView.tsx +164 -0
  63. package/src/viewer/components/ComponentGraph.tsx +111 -142
  64. package/src/viewer/components/ContractPanel.tsx +6 -6
  65. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  66. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  67. package/src/viewer/components/FragmentEditor.tsx +92 -115
  68. package/src/viewer/components/HeaderSearch.tsx +24 -0
  69. package/src/viewer/components/HealthDashboard.tsx +16 -2
  70. package/src/viewer/components/Icons.tsx +9 -0
  71. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  72. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  73. package/src/viewer/components/LandingPage.tsx +3 -3
  74. package/src/viewer/components/LeftSidebar.tsx +141 -63
  75. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  76. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  77. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  78. package/src/viewer/components/PanelShell.tsx +161 -0
  79. package/src/viewer/components/PerformancePanel.tsx +31 -28
  80. package/src/viewer/components/PreviewArea.tsx +1 -1
  81. package/src/viewer/components/PreviewAside.tsx +168 -0
  82. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  83. package/src/viewer/components/PropsEditor.tsx +70 -156
  84. package/src/viewer/components/ResizablePanel.tsx +103 -263
  85. package/src/viewer/components/RightSidebar.tsx +3 -9
  86. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  87. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  88. package/src/viewer/components/TopToolbar.tsx +159 -0
  89. package/src/viewer/components/VariantMatrix.tsx +42 -86
  90. package/src/viewer/components/VariantTabs.tsx +3 -3
  91. package/src/viewer/components/ViewerHeader.tsx +69 -0
  92. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  93. package/src/viewer/components/viewer-utils.ts +16 -0
  94. package/src/viewer/entry.tsx +5 -0
  95. package/src/viewer/hooks/useAppState.ts +27 -4
  96. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  97. package/src/viewer/preview-frame.html +6 -12
  98. package/src/viewer/server.ts +184 -6
  99. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  100. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  101. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  102. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  105. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  108. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  109. package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
  110. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
  111. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
  112. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
  113. package/src/viewer/vendor/shared/src/index.ts +8 -0
  114. package/src/viewer/vendor/shared/src/types.ts +12 -0
  115. package/src/viewer/vite-plugin.ts +109 -4
  116. package/dist/chunk-2JIKCJX3.js.map +0 -1
  117. package/dist/chunk-CJEGT3WD.js.map +0 -1
  118. package/dist/chunk-GOVI6COW.js.map +0 -1
  119. package/dist/generate-35OIMW4Y.js +0 -252
  120. package/dist/generate-35OIMW4Y.js.map +0 -1
  121. package/dist/init-KFYN37ZY.js.map +0 -1
  122. package/dist/viewer-HZK4BSDK.js.map +0 -1
  123. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  124. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  125. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  126. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  127. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  128. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -9,9 +9,11 @@
9
9
  */
10
10
 
11
11
  import { useState, useEffect, useCallback } from "react";
12
- import { Badge } from "@fragments-sdk/ui";
12
+ import { Badge, Stack, Text, Button, Box } from "@fragments-sdk/ui";
13
13
  import type { DesignToken, EnhancedStyleDiffItem, TokenUsageSummary } from "../../core/index.js";
14
+ import { Palette } from "@phosphor-icons/react";
14
15
  import { CheckIcon, XIcon, LoadingIcon, FigmaIcon, WandIcon } from "./Icons.js";
16
+ import { PanelShell } from "./PanelShell.js";
15
17
 
16
18
  // Alias for semantic clarity
17
19
  const SyncingIcon = LoadingIcon;
@@ -241,50 +243,29 @@ export function TokenStylePanel({
241
243
  setTimeout(() => setCopiedFix(null), 2000);
242
244
  }, []);
243
245
 
244
- // Show loading state
245
- if (figmaLoading) {
246
- return (
247
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-tertiary)', padding: '16px' }}>
248
- <SyncingIcon style={{ width: 16, height: 16, animation: 'spin 1s linear infinite' }} />
249
- <span style={{ fontSize: '12px' }}>Loading Figma styles...</span>
250
- </div>
251
- );
252
- }
253
-
254
- // Show error state
255
- if (figmaError) {
256
- return (
257
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-tertiary)', padding: '16px' }}>
258
- <XIcon style={{ width: 16, height: 16, color: '#ef4444' }} />
259
- <span style={{ fontSize: '12px' }}>{figmaError}</span>
260
- <button
261
- onClick={onFetchFigma}
262
- style={{ fontSize: '12px', color: 'var(--color-accent)', background: 'none', border: 'none', cursor: 'pointer', marginLeft: '8px', textDecoration: 'underline' }}
263
- >
264
- Retry
265
- </button>
266
- </div>
267
- );
268
- }
269
-
270
- // Show idle state
271
- if (!figmaStyles) {
272
- return (
273
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-tertiary)', padding: '16px' }}>
274
- <FigmaIcon style={{ width: 16, height: 16 }} />
275
- <span style={{ fontSize: '12px' }}>Click to load style comparison</span>
276
- <button
277
- onClick={onFetchFigma}
278
- style={{ fontSize: '12px', color: 'var(--color-accent)', background: 'none', border: 'none', cursor: 'pointer', marginLeft: '8px', textDecoration: 'underline' }}
279
- >
280
- Load
281
- </button>
282
- </div>
283
- );
284
- }
246
+ // Determine empty config for error / idle states
247
+ const emptyConfig = (() => {
248
+ if (!figmaLoading && figmaError) {
249
+ return {
250
+ icon: <Palette size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
251
+ title: "Style comparison failed",
252
+ description: figmaError,
253
+ action: <Button variant="secondary" size="sm" onClick={onFetchFigma}>Retry</Button>,
254
+ };
255
+ }
256
+ if (!figmaLoading && !figmaStyles) {
257
+ return {
258
+ icon: <Palette size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
259
+ title: "No style data",
260
+ description: "Load Figma styles to compare design tokens with rendered output.",
261
+ action: <Button size="sm" onClick={onFetchFigma}>Load Styles</Button>,
262
+ };
263
+ }
264
+ return undefined;
265
+ })();
285
266
 
286
- // Build comparison data
287
- const figma = figmaStyles;
267
+ // Build comparison data (safe — only used when figmaStyles exist, i.e. PanelShell renders children)
268
+ const figma = figmaStyles || {};
288
269
  const rendered = renderedStyles || {};
289
270
  const allProps = [...new Set([...Object.keys(figma), ...Object.keys(rendered)])];
290
271
 
@@ -293,7 +274,6 @@ export function TokenStylePanel({
293
274
  const renderedValue = rendered[prop] || "(not set)";
294
275
  const match = compareValue(prop, figma[prop] || "", rendered[prop] || "");
295
276
 
296
- // Find matching tokens (with category-aware filtering)
297
277
  const figmaMatch = figma[prop] ? tokenByValue(figma[prop], prop) : null;
298
278
  const renderedMatch = rendered[prop] ? tokenByValue(rendered[prop], prop) : null;
299
279
 
@@ -302,7 +282,6 @@ export function TokenStylePanel({
302
282
  const figmaConfidence = figmaMatch?.confidence;
303
283
  const renderedConfidence = renderedMatch?.confidence;
304
284
 
305
- // Determine if hardcoded (Figma uses a token but rendered doesn't)
306
285
  const isHardcoded = !!figmaToken && !renderedToken && figma[prop] !== rendered[prop];
307
286
 
308
287
  return {
@@ -323,184 +302,178 @@ export function TokenStylePanel({
323
302
  const fixableCount = properties.filter((p) => p.isHardcoded && p.figmaToken).length;
324
303
  const tokenUsageCount = properties.filter((p) => p.renderedToken).length;
325
304
 
326
- // Calculate compliance
327
305
  const compliancePercent =
328
306
  properties.length > 0
329
307
  ? Math.round(((tokenUsageCount + properties.filter((p) => p.match && !p.figmaToken).length) / properties.length) * 100)
330
308
  : 100;
331
309
 
332
- return (
333
- <div style={{ overflowX: 'auto', padding: '16px' }}>
334
- {/* Summary Header */}
335
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
336
- <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
337
- <span style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-primary)' }}>Style Comparison</span>
338
-
339
- {/* Token info badge */}
340
- {tokenData && (
341
- <span style={{ fontSize: '12px', color: 'var(--text-tertiary)' }}>
342
- {tokenData.meta?.totalTokens || tokenData.tokens.length} tokens loaded
343
- </span>
344
- )}
345
- {tokenLoading && (
346
- <span style={{ fontSize: '12px', color: 'var(--text-tertiary)', display: 'flex', alignItems: 'center', gap: '4px' }}>
347
- <SyncingIcon style={{ width: 12, height: 12, animation: 'spin 1s linear infinite' }} />
348
- Loading tokens...
349
- </span>
350
- )}
351
- {tokenError && !tokenLoading && (
352
- <span style={{ fontSize: '12px', color: '#d97706' }} title={tokenError}>
353
- Tokens not configured
354
- </span>
355
- )}
356
- </div>
357
-
358
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
359
- {/* Compliance badge */}
360
- {tokenData && (
361
- <Badge
362
- variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : 'danger'}
363
- >
364
- {compliancePercent}% token compliance
365
- </Badge>
366
- )}
367
-
368
- {/* Match summary */}
369
- <Badge variant={differences === 0 ? 'success' : 'warning'}>
370
- {differences === 0 ? "All styles match" : `${differences} difference${differences !== 1 ? "s" : ""}`}
310
+ // Toolbar with token status + summary badges (only when data is loaded)
311
+ const toolbar = figmaStyles ? (
312
+ <Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
313
+ <Stack direction="row" align="center" gap="md">
314
+ {tokenData && (
315
+ <Text size="xs" color="tertiary">
316
+ {tokenData.meta?.totalTokens || tokenData.tokens.length} tokens loaded
317
+ </Text>
318
+ )}
319
+ {tokenLoading && (
320
+ <Stack direction="row" align="center" gap="xs" style={{ color: 'var(--text-tertiary)' }}>
321
+ <SyncingIcon style={{ width: 12, height: 12, animation: 'spin 1s linear infinite' }} />
322
+ <Text size="xs">Loading tokens...</Text>
323
+ </Stack>
324
+ )}
325
+ {tokenError && !tokenLoading && (
326
+ <Text size="xs" style={{ color: '#d97706' }} title={tokenError}>
327
+ Tokens not configured
328
+ </Text>
329
+ )}
330
+ </Stack>
331
+
332
+ <Stack direction="row" align="center" gap="sm">
333
+ {tokenData && (
334
+ <Badge
335
+ variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : 'danger'}
336
+ >
337
+ {compliancePercent}% token compliance
371
338
  </Badge>
339
+ )}
372
340
 
373
- {/* Hardcoded warning */}
374
- {hardcodedCount > 0 && (
375
- <Badge variant="danger">
376
- {hardcodedCount} hardcoded
377
- </Badge>
378
- )}
379
- </div>
380
- </div>
341
+ <Badge variant={differences === 0 ? 'success' : 'warning'}>
342
+ {differences === 0 ? "All styles match" : `${differences} difference${differences !== 1 ? "s" : ""}`}
343
+ </Badge>
381
344
 
382
- {/* Comparison Table */}
383
- <table style={{ width: '100%', fontSize: '12px' }}>
384
- <thead>
385
- <tr style={{ borderBottom: '1px solid var(--border)' }}>
386
- <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Property</th>
387
- <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Figma</th>
388
- {tokenData && (
389
- <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Token</th>
390
- )}
391
- <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Rendered</th>
392
- <th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--text-tertiary)', fontWeight: 500, width: '48px' }}>Match</th>
393
- {tokenData && fixableCount > 0 && (
394
- <th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--text-tertiary)', fontWeight: 500, width: '64px' }}>Fix</th>
395
- )}
396
- </tr>
397
- </thead>
398
- <tbody>
399
- {properties.map((prop, index) => (
400
- <tr
401
- key={prop.property}
402
- style={{
403
- borderBottom: index < properties.length - 1 ? '1px solid var(--border)' : undefined,
404
- background: prop.isHardcoded
405
- ? 'color-mix(in srgb, #ef4444 5%, transparent)'
406
- : !prop.match
407
- ? 'color-mix(in srgb, #f59e0b 5%, transparent)'
408
- : undefined,
409
- }}
410
- >
411
- <td style={{ padding: '8px 12px', fontFamily: 'monospace', color: 'var(--text-primary)' }}>{prop.property}</td>
412
- <td style={{ padding: '8px 12px' }}>
413
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
414
- {/* Color swatch */}
415
- {isColorProperty(prop.property) && prop.figma !== "(not set)" && (
416
- <ColorSwatch color={prop.figma} />
417
- )}
418
- <span style={{ fontFamily: 'monospace', color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '120px' }} title={prop.figma}>
419
- {prop.figma}
420
- </span>
421
- </div>
422
- </td>
345
+ {hardcodedCount > 0 && (
346
+ <Badge variant="danger">
347
+ {hardcodedCount} hardcoded
348
+ </Badge>
349
+ )}
350
+ </Stack>
351
+ </Stack>
352
+ ) : undefined;
353
+
354
+ return (
355
+ <PanelShell loading={figmaLoading} toolbar={toolbar} empty={emptyConfig}>
356
+ <div style={{ overflowX: 'auto' }}>
357
+ {/* Comparison Table */}
358
+ <table style={{ width: '100%', fontSize: '12px' }}>
359
+ <thead>
360
+ <tr style={{ borderBottom: '1px solid var(--border)' }}>
361
+ <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Property</th>
362
+ <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Figma</th>
423
363
  {tokenData && (
364
+ <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Token</th>
365
+ )}
366
+ <th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--text-tertiary)', fontWeight: 500 }}>Rendered</th>
367
+ <th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--text-tertiary)', fontWeight: 500, width: '48px' }}>Match</th>
368
+ {tokenData && fixableCount > 0 && (
369
+ <th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--text-tertiary)', fontWeight: 500, width: '64px' }}>Fix</th>
370
+ )}
371
+ </tr>
372
+ </thead>
373
+ <tbody>
374
+ {properties.map((prop, index) => (
375
+ <tr
376
+ key={prop.property}
377
+ style={{
378
+ borderBottom: index < properties.length - 1 ? '1px solid var(--border)' : undefined,
379
+ background: prop.isHardcoded
380
+ ? 'color-mix(in srgb, #ef4444 5%, transparent)'
381
+ : !prop.match
382
+ ? 'color-mix(in srgb, #f59e0b 5%, transparent)'
383
+ : undefined,
384
+ }}
385
+ >
386
+ <td style={{ padding: '8px 12px', fontFamily: 'monospace', color: 'var(--text-primary)' }}>{prop.property}</td>
424
387
  <td style={{ padding: '8px 12px' }}>
425
- {prop.figmaToken ? (
426
- <TokenBadge token={prop.figmaToken} confidence={prop.figmaConfidence} />
427
- ) : prop.renderedToken ? (
428
- <TokenBadge token={prop.renderedToken} confidence={prop.renderedConfidence} />
429
- ) : (
430
- <span style={{ color: 'var(--text-tertiary)' }}>-</span>
431
- )}
388
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
389
+ {isColorProperty(prop.property) && prop.figma !== "(not set)" && (
390
+ <ColorSwatch color={prop.figma} />
391
+ )}
392
+ <span style={{ fontFamily: 'monospace', color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '120px' }} title={prop.figma}>
393
+ {prop.figma}
394
+ </span>
395
+ </div>
432
396
  </td>
433
- )}
434
- <td style={{ padding: '8px 12px' }}>
435
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
436
- {/* Color swatch */}
437
- {isColorProperty(prop.property) && prop.rendered !== "(not set)" && (
438
- <ColorSwatch color={prop.rendered} />
439
- )}
440
- <span style={{ fontFamily: 'monospace', color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '120px' }} title={prop.rendered}>
441
- {prop.rendered}
442
- </span>
443
- {/* Hardcoded badge */}
444
- {prop.isHardcoded && (
445
- <Badge variant="danger">HC</Badge>
446
- )}
447
- </div>
448
- </td>
449
- <td style={{ padding: '8px 12px', textAlign: 'center' }}>
450
- {prop.match ? (
451
- <CheckIcon style={{ width: 16, height: 16, color: '#16a34a', margin: '0 auto' }} />
452
- ) : (
453
- <XIcon style={{ width: 16, height: 16, color: '#d97706', margin: '0 auto' }} />
397
+ {tokenData && (
398
+ <td style={{ padding: '8px 12px' }}>
399
+ {prop.figmaToken ? (
400
+ <TokenBadge token={prop.figmaToken} confidence={prop.figmaConfidence} />
401
+ ) : prop.renderedToken ? (
402
+ <TokenBadge token={prop.renderedToken} confidence={prop.renderedConfidence} />
403
+ ) : (
404
+ <span style={{ color: 'var(--text-tertiary)' }}>-</span>
405
+ )}
406
+ </td>
454
407
  )}
455
- </td>
456
- {tokenData && fixableCount > 0 && (
408
+ <td style={{ padding: '8px 12px' }}>
409
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
410
+ {isColorProperty(prop.property) && prop.rendered !== "(not set)" && (
411
+ <ColorSwatch color={prop.rendered} />
412
+ )}
413
+ <span style={{ fontFamily: 'monospace', color: 'var(--text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '120px' }} title={prop.rendered}>
414
+ {prop.rendered}
415
+ </span>
416
+ {prop.isHardcoded && (
417
+ <Badge variant="danger">HC</Badge>
418
+ )}
419
+ </div>
420
+ </td>
457
421
  <td style={{ padding: '8px 12px', textAlign: 'center' }}>
458
- {prop.isHardcoded && prop.figmaToken && (
459
- <button
460
- onClick={() => copyFix(prop.property, prop.figmaToken!.name)}
461
- style={{
462
- padding: '4px',
463
- borderRadius: '4px',
464
- transition: 'color 0.15s, background 0.15s',
465
- background: copiedFix === prop.property ? 'color-mix(in srgb, #22c55e 15%, transparent)' : 'transparent',
466
- color: copiedFix === prop.property ? '#16a34a' : 'var(--text-tertiary)',
467
- border: 'none',
468
- cursor: 'pointer',
469
- }}
470
- title={`Copy: ${prop.property.replace(/([A-Z])/g, "-$1").toLowerCase()}: var(${prop.figmaToken.name});`}
471
- >
472
- {copiedFix === prop.property ? (
473
- <CheckIcon style={{ width: 12, height: 12 }} />
474
- ) : (
475
- <WandIcon style={{ width: 12, height: 12 }} />
476
- )}
477
- </button>
422
+ {prop.match ? (
423
+ <CheckIcon style={{ width: 16, height: 16, color: '#16a34a', margin: '0 auto' }} />
424
+ ) : (
425
+ <XIcon style={{ width: 16, height: 16, color: '#d97706', margin: '0 auto' }} />
478
426
  )}
479
427
  </td>
480
- )}
481
- </tr>
482
- ))}
483
- </tbody>
484
- </table>
485
-
486
- {/* Hardcoded fixes summary */}
487
- {hardcodedCount > 0 && tokenData && (
488
- <div style={{ marginTop: '16px', padding: '12px', background: 'color-mix(in srgb, #f59e0b 8%, transparent)', borderRadius: '8px', border: '1px solid color-mix(in srgb, #f59e0b 20%, transparent)' }}>
489
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
490
- <WandIcon style={{ width: 16, height: 16, color: '#d97706', marginTop: '2px' }} />
491
- <div>
492
- <p style={{ fontSize: '12px', fontWeight: 500, color: '#92400e' }}>
493
- {hardcodedCount} hardcoded value{hardcodedCount !== 1 ? "s" : ""} detected
494
- </p>
495
- <p style={{ fontSize: '12px', color: '#a16207', marginTop: '4px' }}>
496
- These values should use design tokens for consistency and theming support.
497
- Click the fix button to copy the token-based CSS.
498
- </p>
499
- </div>
500
- </div>
501
- </div>
502
- )}
503
- </div>
428
+ {tokenData && fixableCount > 0 && (
429
+ <td style={{ padding: '8px 12px', textAlign: 'center' }}>
430
+ {prop.isHardcoded && prop.figmaToken && (
431
+ <button
432
+ onClick={() => copyFix(prop.property, prop.figmaToken!.name)}
433
+ style={{
434
+ padding: '4px',
435
+ borderRadius: '4px',
436
+ transition: 'color 0.15s, background 0.15s',
437
+ background: copiedFix === prop.property ? 'color-mix(in srgb, #22c55e 15%, transparent)' : 'transparent',
438
+ color: copiedFix === prop.property ? '#16a34a' : 'var(--text-tertiary)',
439
+ border: 'none',
440
+ cursor: 'pointer',
441
+ }}
442
+ title={`Copy: ${prop.property.replace(/([A-Z])/g, "-$1").toLowerCase()}: var(${prop.figmaToken.name});`}
443
+ >
444
+ {copiedFix === prop.property ? (
445
+ <CheckIcon style={{ width: 12, height: 12 }} />
446
+ ) : (
447
+ <WandIcon style={{ width: 12, height: 12 }} />
448
+ )}
449
+ </button>
450
+ )}
451
+ </td>
452
+ )}
453
+ </tr>
454
+ ))}
455
+ </tbody>
456
+ </table>
457
+
458
+ {/* Hardcoded fixes summary */}
459
+ {hardcodedCount > 0 && tokenData && (
460
+ <Box padding="sm" rounded="md" style={{ marginTop: '16px', background: 'color-mix(in srgb, #f59e0b 8%, transparent)', border: '1px solid color-mix(in srgb, #f59e0b 20%, transparent)' }}>
461
+ <Stack direction="row" align="start" gap="sm">
462
+ <WandIcon style={{ width: 16, height: 16, color: '#d97706', marginTop: '2px' }} />
463
+ <div>
464
+ <Text size="xs" weight="medium" style={{ color: '#92400e' }}>
465
+ {hardcodedCount} hardcoded value{hardcodedCount !== 1 ? "s" : ""} detected
466
+ </Text>
467
+ <Text size="xs" style={{ color: '#a16207', marginTop: '4px' }}>
468
+ These values should use design tokens for consistency and theming support.
469
+ Click the fix button to copy the token-based CSS.
470
+ </Text>
471
+ </div>
472
+ </Stack>
473
+ </Box>
474
+ )}
475
+ </div>
476
+ </PanelShell>
504
477
  );
505
478
  }
506
479
 
@@ -0,0 +1,159 @@
1
+ import type { RefObject } from "react";
2
+ import type { FragmentDefinition } from "../../core/index.js";
3
+ import type { useViewSettings } from "../hooks/useViewSettings.js";
4
+ import type { useAppState } from "../hooks/useAppState.js";
5
+ import {
6
+ Header,
7
+ Stack,
8
+ Text,
9
+ Separator,
10
+ Tooltip,
11
+ Button,
12
+ ThemeToggle,
13
+ FragmentsLogo,
14
+ } from "@fragments-sdk/ui";
15
+ import { DeviceMobile, GridFour, SidebarSimple } from "@phosphor-icons/react";
16
+ import { GitHubIcon, FigmaIcon, CompareIcon } from "./Icons.js";
17
+ import { PreviewToolbar } from "./PreviewToolbar.js";
18
+ import { HeaderSearch } from "./HeaderSearch.js";
19
+ import { useTheme } from "./ThemeProvider.js";
20
+ import { WebMCPStatusIndicator } from "./WebMCPStatusIndicator.js";
21
+
22
+ /** Normalize category to Title Case for display */
23
+ function titleCase(str: string): string {
24
+ return str.replace(/\b\w/g, (c) => c.toUpperCase());
25
+ }
26
+
27
+ interface TopToolbarProps {
28
+ fragment: { path: string; fragment: FragmentDefinition };
29
+ viewSettings: ReturnType<typeof useViewSettings>;
30
+ uiState: ReturnType<typeof useAppState>["state"];
31
+ uiActions: ReturnType<typeof useAppState>["actions"];
32
+ figmaUrl?: string;
33
+ searchQuery: string;
34
+ onSearchChange: (value: string) => void;
35
+ searchInputRef: RefObject<HTMLInputElement>;
36
+ }
37
+
38
+ export function TopToolbar({
39
+ fragment,
40
+ viewSettings,
41
+ uiState,
42
+ uiActions,
43
+ figmaUrl,
44
+ searchQuery,
45
+ onSearchChange,
46
+ searchInputRef,
47
+ }: TopToolbarProps) {
48
+ const { setTheme, resolvedTheme } = useTheme();
49
+ return (
50
+ <Header aria-label="Component preview toolbar">
51
+ <Header.Trigger />
52
+ <Header.Brand>
53
+ <Stack direction="row" align="center" gap="sm">
54
+ <FragmentsLogo size={20} />
55
+ <Text weight="medium" size="sm">
56
+ {fragment.fragment.meta.name}
57
+ </Text>
58
+ <Text size="xs" color="tertiary">
59
+ {titleCase(fragment.fragment.meta.category || '')}
60
+ </Text>
61
+ </Stack>
62
+ </Header.Brand>
63
+ <HeaderSearch value={searchQuery} onChange={onSearchChange} inputRef={searchInputRef} />
64
+ <Header.Spacer />
65
+ <Header.Actions>
66
+ <PreviewToolbar zoom={viewSettings.zoom} onZoomChange={viewSettings.setZoom} />
67
+ <Separator orientation="vertical" style={{ height: "16px" }} />
68
+ <Tooltip content={uiState.showMatrixView ? "Disable matrix view" : "Enable matrix view"}>
69
+ <Button
70
+ variant={uiState.showMatrixView ? "secondary" : "ghost"}
71
+ size="sm"
72
+ icon
73
+ aria-pressed={uiState.showMatrixView}
74
+ aria-label="Toggle matrix view"
75
+ onClick={() => uiActions.setMatrixView(!uiState.showMatrixView)}
76
+ >
77
+ <GridFour size={16} />
78
+ </Button>
79
+ </Tooltip>
80
+ <Tooltip
81
+ content={uiState.showMultiViewport ? "Disable responsive view" : "Enable responsive view"}
82
+ >
83
+ <Button
84
+ variant={uiState.showMultiViewport ? "secondary" : "ghost"}
85
+ size="sm"
86
+ icon
87
+ aria-pressed={uiState.showMultiViewport}
88
+ aria-label="Toggle responsive view"
89
+ onClick={() => uiActions.setMultiViewport(!uiState.showMultiViewport)}
90
+ >
91
+ <DeviceMobile size={16} />
92
+ </Button>
93
+ </Tooltip>
94
+ <Separator orientation="vertical" style={{ height: "16px" }} />
95
+ {figmaUrl && (
96
+ <>
97
+ <Tooltip
98
+ content={
99
+ uiState.showComparison ? "Hide Figma comparison" : "Compare with Figma design"
100
+ }
101
+ >
102
+ <Button
103
+ variant={uiState.showComparison ? "secondary" : "ghost"}
104
+ size="sm"
105
+ icon
106
+ onClick={uiActions.toggleComparison}
107
+ >
108
+ <CompareIcon style={{ width: "16px", height: "16px" }} />
109
+ </Button>
110
+ </Tooltip>
111
+ <Tooltip content="View in Figma">
112
+ <Button
113
+ onClick={() => window.open(figmaUrl, "_blank", "noopener,noreferrer")}
114
+ variant="ghost"
115
+ size="sm"
116
+ icon
117
+ >
118
+ <FigmaIcon style={{ width: "16px", height: "16px" }} />
119
+ </Button>
120
+ </Tooltip>
121
+ <Separator orientation="vertical" style={{ height: "16px" }} />
122
+ </>
123
+ )}
124
+ <WebMCPStatusIndicator />
125
+ <Tooltip content={uiState.showAside ? "Hide side panel" : "Show side panel"}>
126
+ <Button
127
+ variant={uiState.showAside ? "secondary" : "ghost"}
128
+ size="sm"
129
+ icon
130
+ aria-pressed={uiState.showAside}
131
+ aria-label="Toggle side panel"
132
+ onClick={uiActions.toggleAside}
133
+ >
134
+ <SidebarSimple size={16} style={{ transform: "scaleX(-1)" }} />
135
+ </Button>
136
+ </Tooltip>
137
+ <Separator orientation="vertical" style={{ height: "16px" }} />
138
+ <ThemeToggle
139
+ size="sm"
140
+ value={resolvedTheme}
141
+ onValueChange={(value) => setTheme(value)}
142
+ aria-label={`Theme: ${resolvedTheme}`}
143
+ />
144
+ <Button
145
+ as="a"
146
+ variant="ghost"
147
+ size="sm"
148
+ icon
149
+ href="https://github.com/ConanMcN/fragments"
150
+ target="_blank"
151
+ rel="noopener noreferrer"
152
+ aria-label="View on GitHub"
153
+ >
154
+ <GitHubIcon />
155
+ </Button>
156
+ </Header.Actions>
157
+ </Header>
158
+ );
159
+ }