@fragments-sdk/cli 0.6.0 → 0.7.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 +529 -285
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-F7ITZPDJ.js → chunk-32VIEOQY.js} +18 -18
- package/dist/chunk-32VIEOQY.js.map +1 -0
- package/dist/{chunk-SSLQXHNX.js → chunk-5ITIP3ES.js} +27 -27
- package/dist/chunk-5ITIP3ES.js.map +1 -0
- package/dist/{chunk-RVRTRESS.js → chunk-DQHWLAUV.js} +29 -29
- package/dist/chunk-DQHWLAUV.js.map +1 -0
- package/dist/{chunk-Q7GOHVOK.js → chunk-GCZMFLDI.js} +67 -32
- package/dist/chunk-GCZMFLDI.js.map +1 -0
- package/dist/{chunk-6JBGU74P.js → chunk-GHYYFAQN.js} +23 -23
- package/dist/chunk-GHYYFAQN.js.map +1 -0
- package/dist/{chunk-NWQ4CJOQ.js → chunk-GKX2HPZ6.js} +40 -40
- package/dist/chunk-GKX2HPZ6.js.map +1 -0
- package/dist/{chunk-D35RGPAG.js → chunk-U6VTHBNI.js} +499 -83
- package/dist/chunk-U6VTHBNI.js.map +1 -0
- package/dist/{core-SKRPJQZG.js → core-SFHPYR5H.js} +24 -26
- package/dist/{generate-7AF7WRVK.js → generate-54GJAWUY.js} +5 -5
- package/dist/generate-54GJAWUY.js.map +1 -0
- package/dist/index.d.ts +23 -27
- package/dist/index.js +10 -10
- package/dist/{init-WKGDPYI4.js → init-EIM5WNMP.js} +5 -5
- package/dist/{init-WKGDPYI4.js.map → init-EIM5WNMP.js.map} +1 -1
- package/dist/mcp-bin.js +73 -73
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-KQBKUS64.js +12 -0
- package/dist/{service-F3E4JJM7.js → service-ED2LNCTU.js} +6 -6
- package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-Q4F4QP5M.js} +4 -4
- package/dist/{test-CJDNJTPZ.js → test-6VN2DA3S.js} +19 -19
- package/dist/test-6VN2DA3S.js.map +1 -0
- package/dist/{tokens-JAJABYXP.js → tokens-P2B7ZAM3.js} +5 -5
- package/dist/{viewer-R3Q6WAMJ.js → viewer-GM7IQPPB.js} +199 -199
- package/dist/viewer-GM7IQPPB.js.map +1 -0
- package/package.json +2 -2
- package/src/ai.ts +5 -5
- package/src/analyze.ts +11 -11
- package/src/bin.ts +24 -1
- package/src/build.ts +64 -21
- package/src/commands/a11y.ts +6 -6
- package/src/commands/add.ts +11 -11
- package/src/commands/audit.ts +4 -4
- package/src/commands/baseline.ts +3 -3
- package/src/commands/build.ts +8 -8
- package/src/commands/compare.ts +20 -20
- package/src/commands/context.ts +16 -16
- package/src/commands/enhance.ts +36 -36
- package/src/commands/generate.ts +1 -1
- package/src/commands/graph.ts +274 -0
- package/src/commands/init.ts +1 -1
- package/src/commands/link/figma.ts +82 -82
- package/src/commands/link/index.ts +3 -3
- package/src/commands/link/storybook.ts +9 -9
- package/src/commands/list.ts +2 -2
- package/src/commands/reset.ts +15 -15
- package/src/commands/scan.ts +27 -27
- package/src/commands/storygen.ts +24 -24
- package/src/commands/validate.ts +2 -2
- package/src/commands/verify.ts +8 -8
- package/src/core/auto-props.ts +4 -4
- package/src/core/composition.test.ts +36 -36
- package/src/core/composition.ts +83 -20
- package/src/core/config.ts +6 -6
- package/src/core/{defineSegment.ts → defineFragment.ts} +16 -22
- package/src/core/discovery.ts +6 -6
- package/src/core/figma.ts +2 -2
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +6 -1
- package/src/core/index.ts +22 -23
- package/src/core/loader.ts +22 -22
- package/src/core/node.ts +5 -5
- package/src/core/parser.ts +31 -31
- package/src/core/previewLoader.ts +1 -1
- package/src/core/schema.ts +16 -16
- package/src/core/storyAdapter.test.ts +87 -87
- package/src/core/storyAdapter.ts +16 -16
- package/src/core/types.ts +21 -26
- package/src/diff.ts +22 -22
- package/src/index.ts +2 -2
- package/src/mcp/server.ts +80 -80
- package/src/migrate/__tests__/utils/utils.test.ts +3 -3
- package/src/migrate/bin.ts +4 -4
- package/src/migrate/converter.ts +16 -16
- package/src/migrate/index.ts +3 -3
- package/src/migrate/migrate.ts +3 -3
- package/src/migrate/parser.ts +8 -8
- package/src/migrate/report.ts +2 -2
- package/src/migrate/types.ts +4 -4
- package/src/screenshot.ts +22 -22
- package/src/service/__tests__/props-extractor.test.ts +15 -15
- package/src/service/analytics.ts +39 -39
- package/src/service/enhance/codebase-scanner.ts +1 -1
- package/src/service/enhance/index.ts +1 -1
- package/src/service/enhance/props-extractor.ts +2 -2
- package/src/service/enhance/types.ts +2 -2
- package/src/service/index.ts +2 -2
- package/src/service/metrics-store.ts +1 -1
- package/src/service/patch-generator.ts +1 -1
- package/src/setup.ts +52 -52
- package/src/shared/dev-server-client.ts +7 -7
- package/src/shared/fragment-loader.ts +59 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/types.ts +4 -4
- package/src/static-viewer.ts +35 -35
- package/src/test/discovery.ts +6 -6
- package/src/test/index.ts +5 -5
- package/src/test/reporters/console.ts +1 -1
- package/src/test/reporters/junit.ts +1 -1
- package/src/test/runner.ts +7 -7
- package/src/test/types.ts +3 -3
- package/src/test/watch.ts +9 -9
- package/src/validators.ts +26 -26
- package/src/viewer/__tests__/render-utils.test.ts +28 -28
- package/src/viewer/__tests__/viewer-integration.test.ts +4 -4
- package/src/viewer/cli/health.ts +26 -26
- package/src/viewer/components/App.tsx +201 -103
- package/src/viewer/components/BottomPanel.tsx +17 -17
- package/src/viewer/components/CodePanel.tsx +3 -3
- package/src/viewer/components/CommandPalette.tsx +11 -11
- package/src/viewer/components/ComponentGraph.tsx +28 -28
- package/src/viewer/components/ComponentHeader.tsx +2 -2
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/FigmaEmbed.tsx +9 -9
- package/src/viewer/components/HealthDashboard.tsx +17 -17
- package/src/viewer/components/Icons.tsx +53 -1
- package/src/viewer/components/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/Layout.tsx +7 -3
- package/src/viewer/components/LeftSidebar.tsx +92 -114
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +77 -48
- package/src/viewer/components/PreviewToolbar.tsx +57 -10
- package/src/viewer/components/RightSidebar.tsx +9 -9
- package/src/viewer/components/Sidebar.tsx +17 -17
- package/src/viewer/components/StoryRenderer.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/UsageSection.tsx +2 -2
- package/src/viewer/components/VariantMatrix.tsx +11 -11
- package/src/viewer/components/VariantRenderer.tsx +3 -3
- package/src/viewer/components/VariantTabs.tsx +2 -2
- package/src/viewer/components/ViewportSelector.tsx +56 -45
- package/src/viewer/components/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- package/src/viewer/constants/ui.ts +4 -4
- package/src/viewer/entry.tsx +40 -40
- package/src/viewer/hooks/useFigmaIntegration.ts +1 -1
- package/src/viewer/hooks/usePreviewBridge.ts +5 -5
- package/src/viewer/hooks/useUrlState.ts +6 -6
- package/src/viewer/index.ts +2 -2
- package/src/viewer/intelligence/healthReport.ts +17 -17
- package/src/viewer/intelligence/styleDrift.ts +1 -1
- package/src/viewer/intelligence/usageScanner.ts +1 -1
- package/src/viewer/preview-frame.html +22 -13
- package/src/viewer/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- package/src/viewer/styles/globals.css +42 -81
- package/src/viewer/utils/detectRelationships.ts +22 -22
- package/src/viewer/vite-plugin.ts +213 -213
- package/dist/chunk-6JBGU74P.js.map +0 -1
- package/dist/chunk-D35RGPAG.js.map +0 -1
- package/dist/chunk-F7ITZPDJ.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-Q7GOHVOK.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-SSLQXHNX.js.map +0 -1
- package/dist/generate-7AF7WRVK.js.map +0 -1
- package/dist/scan-K6JNMCGM.js +0 -12
- package/dist/test-CJDNJTPZ.js.map +0 -1
- package/dist/viewer-R3Q6WAMJ.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-SKRPJQZG.js.map → core-SFHPYR5H.js.map} +0 -0
- /package/dist/{scan-K6JNMCGM.js.map → scan-KQBKUS64.js.map} +0 -0
- /package/dist/{service-F3E4JJM7.js.map → service-ED2LNCTU.js.map} +0 -0
- /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
- /package/dist/{tokens-JAJABYXP.js.map → tokens-P2B7ZAM3.js.map} +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
|
13
13
|
import { Dialog, Stack, Text, Badge, Separator, Input } from '@fragments/ui';
|
|
14
|
-
import type {
|
|
14
|
+
import type { FragmentDefinition } from "../../core/index.js";
|
|
15
15
|
import { SearchIcon, ChevronRightIcon } from "./Icons.js";
|
|
16
16
|
|
|
17
17
|
interface CommandPaletteProps {
|
|
@@ -19,8 +19,8 @@ interface CommandPaletteProps {
|
|
|
19
19
|
isOpen: boolean;
|
|
20
20
|
/** Callback to close the palette */
|
|
21
21
|
onClose: () => void;
|
|
22
|
-
/** All available
|
|
23
|
-
|
|
22
|
+
/** All available fragments */
|
|
23
|
+
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
24
24
|
/** Callback when a component is selected */
|
|
25
25
|
onSelectComponent: (path: string) => void;
|
|
26
26
|
/** Callback when a variant is selected */
|
|
@@ -40,7 +40,7 @@ interface SearchResult {
|
|
|
40
40
|
export function CommandPalette({
|
|
41
41
|
isOpen,
|
|
42
42
|
onClose,
|
|
43
|
-
|
|
43
|
+
fragments,
|
|
44
44
|
onSelectComponent,
|
|
45
45
|
onSelectVariant,
|
|
46
46
|
}: CommandPaletteProps) {
|
|
@@ -53,9 +53,9 @@ export function CommandPalette({
|
|
|
53
53
|
const results = useMemo(() => {
|
|
54
54
|
const allResults: SearchResult[] = [];
|
|
55
55
|
|
|
56
|
-
for (const { path,
|
|
57
|
-
const componentName =
|
|
58
|
-
const category =
|
|
56
|
+
for (const { path, fragment } of fragments) {
|
|
57
|
+
const componentName = fragment.meta.name;
|
|
58
|
+
const category = fragment.meta.category;
|
|
59
59
|
|
|
60
60
|
// Add component result
|
|
61
61
|
allResults.push({
|
|
@@ -67,9 +67,9 @@ export function CommandPalette({
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
// Add variant results
|
|
70
|
-
if (
|
|
71
|
-
for (let i = 0; i <
|
|
72
|
-
const variant =
|
|
70
|
+
if (fragment.variants) {
|
|
71
|
+
for (let i = 0; i < fragment.variants.length; i++) {
|
|
72
|
+
const variant = fragment.variants[i];
|
|
73
73
|
allResults.push({
|
|
74
74
|
type: "variant",
|
|
75
75
|
path,
|
|
@@ -106,7 +106,7 @@ export function CommandPalette({
|
|
|
106
106
|
.slice(0, 20);
|
|
107
107
|
|
|
108
108
|
return scored;
|
|
109
|
-
}, [
|
|
109
|
+
}, [fragments, query]);
|
|
110
110
|
|
|
111
111
|
// Reset selection when results change
|
|
112
112
|
useEffect(() => {
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { useMemo, useState } from "react";
|
|
12
|
-
import type {
|
|
12
|
+
import type { FragmentDefinition, ComponentRelation, RelationshipType } from "../../core/index.js";
|
|
13
13
|
import { ChevronRightIcon, EmptyIcon, WandIcon } from "./Icons.js";
|
|
14
14
|
import { detectAllRelationships, mergeRelationships } from "../utils/detectRelationships.js";
|
|
15
15
|
import { Stack, Text, Badge, Button, EmptyState, CodeBlock, Grid, Separator } from "@fragments/ui";
|
|
16
16
|
|
|
17
17
|
interface ComponentGraphProps {
|
|
18
|
-
/** Current
|
|
19
|
-
|
|
20
|
-
/** All available
|
|
21
|
-
|
|
18
|
+
/** Current fragment definition */
|
|
19
|
+
fragment: FragmentDefinition | null;
|
|
20
|
+
/** All available fragments for navigation */
|
|
21
|
+
allFragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
22
22
|
/** Callback when a component is clicked */
|
|
23
23
|
onNavigate?: (componentName: string) => void;
|
|
24
24
|
}
|
|
@@ -68,24 +68,24 @@ const RELATIONSHIP_CONFIG: Record<RelationshipType, { label: string; color: stri
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
export function ComponentGraph({
|
|
71
|
+
export function ComponentGraph({ fragment, allFragments, onNavigate }: ComponentGraphProps) {
|
|
72
72
|
const [showAutoDetected, setShowAutoDetected] = useState(true);
|
|
73
73
|
|
|
74
74
|
// Auto-detect relationships
|
|
75
75
|
const detectedRelationships = useMemo(() => {
|
|
76
|
-
if (!
|
|
76
|
+
if (!fragment) return [];
|
|
77
77
|
try {
|
|
78
|
-
return detectAllRelationships(
|
|
78
|
+
return detectAllRelationships(fragment, allFragments);
|
|
79
79
|
} catch {
|
|
80
80
|
return [];
|
|
81
81
|
}
|
|
82
|
-
}, [
|
|
82
|
+
}, [fragment, allFragments]);
|
|
83
83
|
|
|
84
84
|
// Merge manual and detected relationships
|
|
85
85
|
const allRelationships = useMemo(() => {
|
|
86
|
-
if (!
|
|
87
|
-
return mergeRelationships(
|
|
88
|
-
}, [
|
|
86
|
+
if (!fragment) return [];
|
|
87
|
+
return mergeRelationships(fragment.relations, showAutoDetected ? detectedRelationships : []);
|
|
88
|
+
}, [fragment, detectedRelationships, showAutoDetected]);
|
|
89
89
|
|
|
90
90
|
// Group relations by type
|
|
91
91
|
const groupedRelations = useMemo(() => {
|
|
@@ -110,22 +110,22 @@ export function ComponentGraph({ segment, allSegments, onNavigate }: ComponentGr
|
|
|
110
110
|
|
|
111
111
|
// Find reverse relations (components that reference this one)
|
|
112
112
|
const reverseRelations = useMemo(() => {
|
|
113
|
-
if (!
|
|
113
|
+
if (!fragment) return [];
|
|
114
114
|
|
|
115
|
-
const componentName =
|
|
116
|
-
const reverse: Array<{
|
|
115
|
+
const componentName = fragment.meta.name;
|
|
116
|
+
const reverse: Array<{ fragment: FragmentDefinition; relation: ComponentRelation }> = [];
|
|
117
117
|
|
|
118
|
-
for (const {
|
|
119
|
-
if (
|
|
120
|
-
if (!
|
|
118
|
+
for (const { fragment: otherFragment } of allFragments) {
|
|
119
|
+
if (otherFragment.meta.name === componentName) continue;
|
|
120
|
+
if (!otherFragment.relations) continue;
|
|
121
121
|
|
|
122
|
-
for (const relation of
|
|
122
|
+
for (const relation of otherFragment.relations) {
|
|
123
123
|
if (relation.component === componentName) {
|
|
124
124
|
reverse.push({
|
|
125
|
-
|
|
125
|
+
fragment: otherFragment,
|
|
126
126
|
relation: {
|
|
127
127
|
...relation,
|
|
128
|
-
component:
|
|
128
|
+
component: otherFragment.meta.name,
|
|
129
129
|
// Flip the relationship direction for display
|
|
130
130
|
relationship: flipRelationship(relation.relationship),
|
|
131
131
|
},
|
|
@@ -135,11 +135,11 @@ export function ComponentGraph({ segment, allSegments, onNavigate }: ComponentGr
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
return reverse;
|
|
138
|
-
}, [
|
|
138
|
+
}, [fragment, allFragments]);
|
|
139
139
|
|
|
140
|
-
// Check if a component exists in our
|
|
140
|
+
// Check if a component exists in our fragments
|
|
141
141
|
const componentExists = (name: string) => {
|
|
142
|
-
return
|
|
142
|
+
return allFragments.some(s => s.fragment.meta.name === name);
|
|
143
143
|
};
|
|
144
144
|
|
|
145
145
|
const handleNavigate = (componentName: string) => {
|
|
@@ -148,8 +148,8 @@ export function ComponentGraph({ segment, allSegments, onNavigate }: ComponentGr
|
|
|
148
148
|
}
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
// No
|
|
152
|
-
if (!
|
|
151
|
+
// No fragment selected
|
|
152
|
+
if (!fragment) {
|
|
153
153
|
return (
|
|
154
154
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
155
155
|
<div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
|
|
@@ -184,7 +184,7 @@ export function ComponentGraph({ segment, allSegments, onNavigate }: ComponentGr
|
|
|
184
184
|
</EmptyState.Icon>
|
|
185
185
|
<EmptyState.Title>No relationships defined</EmptyState.Title>
|
|
186
186
|
<EmptyState.Description>
|
|
187
|
-
This component doesn't have any defined relationships. Add a <code style={{ padding: '2px 4px', background: 'var(--bg-secondary)', borderRadius: '4px', fontSize: '12px' }}>relations</code> array to your
|
|
187
|
+
This component doesn't have any defined relationships. Add a <code style={{ padding: '2px 4px', background: 'var(--bg-secondary)', borderRadius: '4px', fontSize: '12px' }}>relations</code> array to your fragment definition to visualize component connections.
|
|
188
188
|
</EmptyState.Description>
|
|
189
189
|
<div style={{ marginTop: '16px', width: '100%' }}>
|
|
190
190
|
<CodeBlock language="typescript">{`relations: [
|
|
@@ -220,7 +220,7 @@ export function ComponentGraph({ segment, allSegments, onNavigate }: ComponentGr
|
|
|
220
220
|
)}
|
|
221
221
|
</Stack>
|
|
222
222
|
<Text size="xs" color="secondary" style={{ marginTop: '4px' }}>
|
|
223
|
-
Relationships for <Text as="span" weight="medium" size="xs">{
|
|
223
|
+
Relationships for <Text as="span" weight="medium" size="xs">{fragment.meta.name}</Text>
|
|
224
224
|
{showAutoDetected && detectedCount > 0 && (
|
|
225
225
|
<span style={{ marginLeft: '4px' }}>
|
|
226
226
|
({detectedCount} from variants)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FragmentMeta } from '../../core/index.js';
|
|
2
2
|
import { Badge, Alert, Text, Stack, Separator } from '@fragments/ui';
|
|
3
3
|
import { WarningIcon, BeakerIcon } from './Icons.js';
|
|
4
4
|
|
|
5
5
|
interface ComponentHeaderProps {
|
|
6
|
-
meta:
|
|
6
|
+
meta: FragmentMeta;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// Map status to Badge variant
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ContractPanel component - displays
|
|
2
|
+
* ContractPanel component - displays FragmentContract metadata for AI agents.
|
|
3
3
|
* Shows propsSummary, scenarioTags, bans, and a11yRules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { memo } from 'react';
|
|
7
|
-
import type {
|
|
7
|
+
import type { FragmentContract } from '../../core/index.js';
|
|
8
8
|
import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge } from '@fragments/ui';
|
|
9
9
|
|
|
10
10
|
interface ContractPanelProps {
|
|
11
|
-
contract?:
|
|
11
|
+
contract?: FragmentContract;
|
|
12
12
|
componentName: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -37,7 +37,7 @@ export const ContractPanel = memo(function ContractPanel({
|
|
|
37
37
|
</EmptyState.Icon>
|
|
38
38
|
<EmptyState.Title>No contract metadata</EmptyState.Title>
|
|
39
39
|
<EmptyState.Description>
|
|
40
|
-
Add a <code style={{ background: 'var(--bg-hover)', padding: '2px 4px', borderRadius: '4px' }}>contract</code> field to the
|
|
40
|
+
Add a <code style={{ background: 'var(--bg-hover)', padding: '2px 4px', borderRadius: '4px' }}>contract</code> field to the fragment definition to enable AI agent features.
|
|
41
41
|
</EmptyState.Description>
|
|
42
42
|
<EmptyState.Actions>
|
|
43
43
|
<CodeBlock
|
|
@@ -186,7 +186,7 @@ function Section({ title, subtitle, children }: SectionProps) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// Check if contract is empty or has no meaningful content
|
|
189
|
-
function isContractEmpty(contract:
|
|
189
|
+
function isContractEmpty(contract: FragmentContract): boolean {
|
|
190
190
|
return (
|
|
191
191
|
(!contract.propsSummary || contract.propsSummary.length === 0) &&
|
|
192
192
|
(!contract.scenarioTags || contract.scenarioTags.length === 0) &&
|
|
@@ -196,7 +196,7 @@ function isContractEmpty(contract: SegmentContract): boolean {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// Check if contract has some but not all fields
|
|
199
|
-
function isPartialContract(contract:
|
|
199
|
+
function isPartialContract(contract: FragmentContract): boolean {
|
|
200
200
|
const fieldCount = [
|
|
201
201
|
contract.propsSummary?.length ?? 0,
|
|
202
202
|
contract.scenarioTags?.length ?? 0,
|
|
@@ -40,7 +40,7 @@ function parseFigmaUrl(figmaUrl: string): ParsedFigmaUrl | null {
|
|
|
40
40
|
* Build a Figma embed URL.
|
|
41
41
|
*/
|
|
42
42
|
function buildEmbedUrl(fileKey: string, nodeId?: string): string {
|
|
43
|
-
let embedUrl = `https://embed.figma.com/design/${fileKey}?embed-host=
|
|
43
|
+
let embedUrl = `https://embed.figma.com/design/${fileKey}?embed-host=fragments`;
|
|
44
44
|
|
|
45
45
|
if (nodeId) {
|
|
46
46
|
const embedNodeId = nodeId.replace(/:/g, "-");
|
|
@@ -205,25 +205,25 @@ export function FigmaEmbed({ figmaUrl, allFigmaUrls, zoom = 100, className, styl
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Hook to collect all Figma URLs from a
|
|
208
|
+
* Hook to collect all Figma URLs from a fragment's variants.
|
|
209
209
|
* This enables the FigmaEmbed to preload all variant iframes.
|
|
210
210
|
*/
|
|
211
211
|
export function useAllFigmaUrls(
|
|
212
|
-
|
|
212
|
+
fragment: { meta: { figma?: string }; variants?: Array<{ figma?: string }> } | undefined
|
|
213
213
|
): string[] {
|
|
214
214
|
return useMemo(() => {
|
|
215
|
-
if (!
|
|
215
|
+
if (!fragment) return [];
|
|
216
216
|
|
|
217
217
|
const urls: string[] = [];
|
|
218
218
|
|
|
219
219
|
// Add meta-level Figma URL
|
|
220
|
-
if (
|
|
221
|
-
urls.push(
|
|
220
|
+
if (fragment.meta.figma) {
|
|
221
|
+
urls.push(fragment.meta.figma);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// Add variant-level Figma URLs
|
|
225
|
-
if (
|
|
226
|
-
for (const variant of
|
|
225
|
+
if (fragment.variants) {
|
|
226
|
+
for (const variant of fragment.variants) {
|
|
227
227
|
if (variant.figma) {
|
|
228
228
|
urls.push(variant.figma);
|
|
229
229
|
}
|
|
@@ -232,5 +232,5 @@ export function useAllFigmaUrls(
|
|
|
232
232
|
|
|
233
233
|
// Deduplicate
|
|
234
234
|
return [...new Set(urls)];
|
|
235
|
-
}, [
|
|
235
|
+
}, [fragment]);
|
|
236
236
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
8
|
-
import type {
|
|
8
|
+
import type { FragmentDefinition } from '../../core/index.js';
|
|
9
9
|
import type { ImpactValue } from 'axe-core';
|
|
10
10
|
import { Badge, Progress, Stack, Text, Card, EmptyState, Table } from '@fragments/ui';
|
|
11
11
|
import {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import type { A11ySummary, CachedA11yResult } from '../types/a11y.js';
|
|
18
18
|
|
|
19
19
|
interface HealthDashboardProps {
|
|
20
|
-
|
|
20
|
+
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
21
21
|
onNavigate?: (componentName: string) => void;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -50,9 +50,9 @@ interface ComponentRow {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function calculateCoverage(
|
|
53
|
-
|
|
53
|
+
fragments: Array<{ path: string; fragment: FragmentDefinition }>
|
|
54
54
|
): { metrics: CoverageMetric[]; components: ComponentRow[]; categoryCount: number } {
|
|
55
|
-
const total =
|
|
55
|
+
const total = fragments.length;
|
|
56
56
|
|
|
57
57
|
if (total === 0) {
|
|
58
58
|
return { metrics: [], components: [], categoryCount: 0 };
|
|
@@ -66,32 +66,32 @@ function calculateCoverage(
|
|
|
66
66
|
let withUsage = 0;
|
|
67
67
|
let figmaLinked = 0;
|
|
68
68
|
|
|
69
|
-
for (const {
|
|
70
|
-
const cat =
|
|
69
|
+
for (const { fragment } of fragments) {
|
|
70
|
+
const cat = fragment.meta.category || 'uncategorized';
|
|
71
71
|
categories.add(cat);
|
|
72
72
|
|
|
73
|
-
if (
|
|
73
|
+
if (fragment.meta.description && fragment.meta.description.trim().length > 10) {
|
|
74
74
|
documented++;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const variantCount =
|
|
77
|
+
const variantCount = fragment.variants?.length || 0;
|
|
78
78
|
if (variantCount > 0) {
|
|
79
79
|
withVariants++;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
if (
|
|
82
|
+
if (fragment.usage && (fragment.usage.when.length > 0 || fragment.usage.whenNot.length > 0)) {
|
|
83
83
|
withUsage++;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
if (
|
|
86
|
+
if (fragment.meta.figma || fragment.variants?.some((v) => v.figma)) {
|
|
87
87
|
figmaLinked++;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
components.push({
|
|
91
|
-
name:
|
|
91
|
+
name: fragment.meta.name,
|
|
92
92
|
category: cat,
|
|
93
93
|
variantCount,
|
|
94
|
-
status:
|
|
94
|
+
status: fragment.meta.status || 'stable',
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -120,10 +120,10 @@ function impactToBadgeVariant(impact: ImpactValue | undefined): 'error' | 'warni
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
export function HealthDashboard({
|
|
123
|
+
export function HealthDashboard({ fragments, onNavigate }: HealthDashboardProps) {
|
|
124
124
|
const { metrics, components, categoryCount } = useMemo(
|
|
125
|
-
() => calculateCoverage(
|
|
126
|
-
[
|
|
125
|
+
() => calculateCoverage(fragments),
|
|
126
|
+
[fragments]
|
|
127
127
|
);
|
|
128
128
|
|
|
129
129
|
const [componentA11y, setComponentA11y] = useState<Record<string, ComponentA11yResult>>({});
|
|
@@ -212,7 +212,7 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
|
|
|
212
212
|
};
|
|
213
213
|
}, [componentA11y, a11ySummary]);
|
|
214
214
|
|
|
215
|
-
if (
|
|
215
|
+
if (fragments.length === 0) {
|
|
216
216
|
return (
|
|
217
217
|
<EmptyState>
|
|
218
218
|
<EmptyState.Description>No components loaded</EmptyState.Description>
|
|
@@ -234,7 +234,7 @@ export function HealthDashboard({ segments, onNavigate }: HealthDashboardProps)
|
|
|
234
234
|
<div>
|
|
235
235
|
<Text as="h1" size="lg" weight="semibold">Fragments</Text>
|
|
236
236
|
<Text size="sm" color="tertiary" style={{ marginTop: '2px' }}>
|
|
237
|
-
{
|
|
237
|
+
{fragments.length} component{fragments.length !== 1 ? 's' : ''} · {categoryCount} categor{categoryCount !== 1 ? 'ies' : 'y'}
|
|
238
238
|
</Text>
|
|
239
239
|
</div>
|
|
240
240
|
|
|
@@ -199,6 +199,49 @@ export function ViewportIcon({ className, style }: IconProps) {
|
|
|
199
199
|
);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
export function ResponsiveIcon({ className, style }: IconProps) {
|
|
203
|
+
return (
|
|
204
|
+
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.7}>
|
|
205
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 12h10m0 0l-2.5-2.5M17 12l-2.5 2.5M7 12l2.5-2.5M7 12l2.5 2.5" />
|
|
206
|
+
</svg>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function DesktopIcon({ className, style }: IconProps) {
|
|
211
|
+
return (
|
|
212
|
+
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
213
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 5.25A2.25 2.25 0 016 3h12a2.25 2.25 0 012.25 2.25v8.25A2.25 2.25 0 0118 15.75H6a2.25 2.25 0 01-2.25-2.25V5.25zM9.75 20.25h4.5M12 15.75v4.5" />
|
|
214
|
+
</svg>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function TabletIcon({ className, style }: IconProps) {
|
|
219
|
+
return (
|
|
220
|
+
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
221
|
+
<rect x="6.75" y="3" width="10.5" height="18" rx="2.25" />
|
|
222
|
+
<circle cx="12" cy="17.25" r="0.85" fill="currentColor" stroke="none" />
|
|
223
|
+
</svg>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function MobileIcon({ className, style }: IconProps) {
|
|
228
|
+
return (
|
|
229
|
+
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
230
|
+
<rect x="8.25" y="2.25" width="7.5" height="19.5" rx="2.25" />
|
|
231
|
+
<circle cx="12" cy="18.75" r="0.8" fill="currentColor" stroke="none" />
|
|
232
|
+
</svg>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function SettingsIcon({ className, style }: IconProps) {
|
|
237
|
+
return (
|
|
238
|
+
<svg className={className} style={style} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
239
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.094c.55 0 1.02.398 1.11.94l.18 1.09c.06.36.29.668.62.84l.94.5c.318.168.7.18 1.028.03l.998-.455c.5-.23 1.09-.048 1.38.42l.546.882c.29.468.19 1.074-.24 1.424l-.858.698a1.26 1.26 0 00-.456.985v1.046c0 .38.17.738.456.984l.858.699c.43.35.53.956.24 1.424l-.545.882a1.1 1.1 0 01-1.381.42l-.998-.455a1.26 1.26 0 00-1.028.03l-.94.5a1.26 1.26 0 00-.62.84l-.18 1.09a1.125 1.125 0 01-1.11.94h-1.094a1.124 1.124 0 01-1.11-.94l-.18-1.09a1.26 1.26 0 00-.62-.84l-.94-.5a1.26 1.26 0 00-1.028-.03l-.998.455a1.1 1.1 0 01-1.381-.42l-.545-.882a1.125 1.125 0 01.24-1.424l.858-.699a1.26 1.26 0 00.456-.984V9.878c0-.38-.17-.738-.456-.985l-.858-.698a1.125 1.125 0 01-.24-1.424l.545-.882c.29-.468.88-.65 1.381-.42l.998.455c.33.15.71.138 1.028-.03l.94-.5c.33-.172.56-.48.62-.84l.18-1.09z" />
|
|
240
|
+
<circle cx="12" cy="12" r="3" />
|
|
241
|
+
</svg>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
202
245
|
// Misc
|
|
203
246
|
export function ControlsIcon({ className, style }: IconProps) {
|
|
204
247
|
return (
|
|
@@ -235,7 +278,16 @@ export function HeartPulseIcon({ className, style }: IconProps) {
|
|
|
235
278
|
|
|
236
279
|
export function DashboardIcon({ className, style }: IconProps) {
|
|
237
280
|
return (
|
|
238
|
-
<svg
|
|
281
|
+
<svg
|
|
282
|
+
className={className}
|
|
283
|
+
style={style}
|
|
284
|
+
width="20"
|
|
285
|
+
height="20"
|
|
286
|
+
fill="none"
|
|
287
|
+
viewBox="0 0 24 24"
|
|
288
|
+
stroke="currentColor"
|
|
289
|
+
strokeWidth={1.5}
|
|
290
|
+
>
|
|
239
291
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
|
240
292
|
</svg>
|
|
241
293
|
);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
13
13
|
import { Button, Stack, Text, Badge, EmptyState, CodeBlock, Alert } from "@fragments/ui";
|
|
14
|
-
import type { PlayFunction, PlayFunctionContext,
|
|
14
|
+
import type { PlayFunction, PlayFunctionContext, FragmentVariant } from "../../core/index.js";
|
|
15
15
|
import {
|
|
16
16
|
PlayIcon,
|
|
17
17
|
CheckIcon,
|
|
@@ -54,7 +54,7 @@ interface DebugState {
|
|
|
54
54
|
|
|
55
55
|
interface InteractionsPanelProps {
|
|
56
56
|
/** The current variant being displayed */
|
|
57
|
-
variant:
|
|
57
|
+
variant: FragmentVariant | null;
|
|
58
58
|
/** Selector for the preview container element */
|
|
59
59
|
previewSelector?: string;
|
|
60
60
|
/** Key that changes when the preview updates */
|
|
@@ -15,8 +15,8 @@ import { usePreviewBridge, type ParentMessage } from '../hooks/usePreviewBridge.
|
|
|
15
15
|
const MAX_RETRIES = 3;
|
|
16
16
|
|
|
17
17
|
export interface IsolatedPreviewFrameProps {
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/** Fragment path (file path) to render */
|
|
19
|
+
fragmentPath: string;
|
|
20
20
|
/** Variant name to render */
|
|
21
21
|
variantName: string;
|
|
22
22
|
/** Props to pass to the variant render function */
|
|
@@ -50,7 +50,7 @@ export interface IsolatedPreviewFrameProps {
|
|
|
50
50
|
* for complete CSS isolation from the viewer shell.
|
|
51
51
|
*/
|
|
52
52
|
export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
53
|
-
|
|
53
|
+
fragmentPath,
|
|
54
54
|
variantName,
|
|
55
55
|
props,
|
|
56
56
|
theme,
|
|
@@ -109,12 +109,12 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
|
|
|
109
109
|
if (!isReady) return;
|
|
110
110
|
|
|
111
111
|
// Create a render key to detect changes
|
|
112
|
-
const renderKey = `${
|
|
112
|
+
const renderKey = `${fragmentPath}:${variantName}:${JSON.stringify(props)}:${previewKey || ''}`;
|
|
113
113
|
if (renderKey === lastRenderRef.current) return;
|
|
114
114
|
lastRenderRef.current = renderKey;
|
|
115
115
|
|
|
116
|
-
render(
|
|
117
|
-
}, [isReady,
|
|
116
|
+
render(fragmentPath, variantName, props);
|
|
117
|
+
}, [isReady, fragmentPath, variantName, props, previewKey, render]);
|
|
118
118
|
|
|
119
119
|
// Sync theme when it changes
|
|
120
120
|
useEffect(() => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useMemo, useEffect, useState } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { FragmentDefinition } from "../../core/index.js";
|
|
3
3
|
import { VariantRenderer } from "./VariantRenderer.js";
|
|
4
4
|
import { getBackgroundStyle, type BackgroundOption, type ZoomLevel } from "./PreviewToolbar.js";
|
|
5
5
|
|
|
6
6
|
interface IsolatedRenderProps {
|
|
7
|
-
|
|
7
|
+
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -12,7 +12,7 @@ interface IsolatedRenderProps {
|
|
|
12
12
|
* Renders a single variant with minimal UI for visual testing.
|
|
13
13
|
* URL params: ?isolated=true&component=Name&variant=VariantName&zoom=100&bg=white&theme=light
|
|
14
14
|
*/
|
|
15
|
-
export function IsolatedRender({
|
|
15
|
+
export function IsolatedRender({ fragments }: IsolatedRenderProps) {
|
|
16
16
|
const [ready, setReady] = useState(false);
|
|
17
17
|
|
|
18
18
|
// Parse query parameters
|
|
@@ -31,21 +31,21 @@ export function IsolatedRender({ segments }: IsolatedRenderProps) {
|
|
|
31
31
|
};
|
|
32
32
|
}, []);
|
|
33
33
|
|
|
34
|
-
// Find the matching
|
|
34
|
+
// Find the matching fragment and variant
|
|
35
35
|
const match = useMemo(() => {
|
|
36
36
|
if (!params.component || !params.variant) {
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
(s) => s.
|
|
40
|
+
const fragment = fragments.find(
|
|
41
|
+
(s) => s.fragment.meta.name === params.component
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
if (!
|
|
44
|
+
if (!fragment) {
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const variant =
|
|
48
|
+
const variant = fragment.fragment.variants.find(
|
|
49
49
|
(v) => v.name === params.variant
|
|
50
50
|
);
|
|
51
51
|
|
|
@@ -53,8 +53,8 @@ export function IsolatedRender({ segments }: IsolatedRenderProps) {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return {
|
|
57
|
-
}, [
|
|
56
|
+
return { fragment: fragment.fragment, variant };
|
|
57
|
+
}, [fragments, params]);
|
|
58
58
|
|
|
59
59
|
// Apply theme
|
|
60
60
|
useEffect(() => {
|
|
@@ -3,13 +3,17 @@ import { AppShell } from '@fragments/ui';
|
|
|
3
3
|
|
|
4
4
|
interface LayoutProps {
|
|
5
5
|
leftSidebar: ReactNode;
|
|
6
|
+
header: ReactNode;
|
|
6
7
|
children: ReactNode;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function Layout({ leftSidebar, children }: LayoutProps) {
|
|
10
|
+
export function Layout({ leftSidebar, header, children }: LayoutProps) {
|
|
10
11
|
return (
|
|
11
|
-
<AppShell layout="
|
|
12
|
-
<AppShell.
|
|
12
|
+
<AppShell layout="inset">
|
|
13
|
+
<AppShell.Header>
|
|
14
|
+
{header}
|
|
15
|
+
</AppShell.Header>
|
|
16
|
+
<AppShell.Sidebar width="256px" collapsible="icon">
|
|
13
17
|
{leftSidebar}
|
|
14
18
|
</AppShell.Sidebar>
|
|
15
19
|
<AppShell.Main padding="none">
|