@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.
- package/dist/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /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
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
{
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
</
|
|
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
|
-
|
|
456
|
-
|
|
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.
|
|
459
|
-
<
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|