@fragments-sdk/cli 0.7.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 +245 -245
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-XHUDJNN3.js → chunk-32VIEOQY.js} +18 -18
- package/dist/chunk-32VIEOQY.js.map +1 -0
- package/dist/{chunk-CVXKXVOY.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-TJ34N7C7.js → chunk-GCZMFLDI.js} +30 -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-7OPWMLOE.js → chunk-U6VTHBNI.js} +110 -110
- package/dist/chunk-U6VTHBNI.js.map +1 -0
- package/dist/{core-W2HYIQW6.js → core-SFHPYR5H.js} +24 -26
- package/dist/{generate-LMTISDIJ.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-7CHRKQ7P.js → init-EIM5WNMP.js} +5 -5
- package/dist/{init-7CHRKQ7P.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-T2L7VLTE.js → service-ED2LNCTU.js} +6 -6
- package/dist/{static-viewer-GBR7YNF3.js → static-viewer-Q4F4QP5M.js} +4 -4
- package/dist/{test-OJRXNDO2.js → test-6VN2DA3S.js} +19 -19
- package/dist/test-6VN2DA3S.js.map +1 -0
- package/dist/{tokens-3BWDESVM.js → tokens-P2B7ZAM3.js} +5 -5
- package/dist/{viewer-SUFOISZM.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 +1 -1
- package/src/build.ts +33 -33
- 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 +3 -3
- 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 +19 -19
- 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 +77 -77
- package/src/core/graph-extractor.ts +32 -32
- package/src/core/importAnalyzer.ts +1 -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 +79 -79
- 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/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/LeftSidebar.tsx +28 -28
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +51 -51
- 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/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- 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/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- 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-7OPWMLOE.js.map +0 -1
- package/dist/chunk-CVXKXVOY.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-TJ34N7C7.js.map +0 -1
- package/dist/chunk-XHUDJNN3.js.map +0 -1
- package/dist/generate-LMTISDIJ.js.map +0 -1
- package/dist/scan-WY23TJCP.js +0 -12
- package/dist/test-OJRXNDO2.js.map +0 -1
- package/dist/viewer-SUFOISZM.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-W2HYIQW6.js.map → core-SFHPYR5H.js.map} +0 -0
- /package/dist/{scan-WY23TJCP.js.map → scan-KQBKUS64.js.map} +0 -0
- /package/dist/{service-T2L7VLTE.js.map → service-ED2LNCTU.js.map} +0 -0
- /package/dist/{static-viewer-GBR7YNF3.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
- /package/dist/{tokens-3BWDESVM.js.map → tokens-P2B7ZAM3.js.map} +0 -0
|
@@ -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
|
|
|
@@ -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(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FragmentDefinition } from '../../core/index.js';
|
|
3
3
|
import { BRAND } from '../../core/index.js';
|
|
4
4
|
import { useTheme } from './ThemeProvider.js';
|
|
5
5
|
import { Sidebar, useSidebar, Badge, Text, ThemeToggle } from '@fragments/ui';
|
|
@@ -45,19 +45,19 @@ function fuzzyMatch(text: string, pattern: string): FuzzyMatch | null {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
interface SearchResult {
|
|
48
|
-
item: { path: string;
|
|
48
|
+
item: { path: string; fragment: FragmentDefinition };
|
|
49
49
|
score: number;
|
|
50
50
|
nameIndices: number[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
item: { path: string;
|
|
53
|
+
function searchFragment(
|
|
54
|
+
item: { path: string; fragment: FragmentDefinition },
|
|
55
55
|
query: string
|
|
56
56
|
): SearchResult | null {
|
|
57
|
-
const {
|
|
58
|
-
// Skip invalid
|
|
59
|
-
if (!
|
|
60
|
-
const { name, category, tags } =
|
|
57
|
+
const { fragment } = item;
|
|
58
|
+
// Skip invalid fragments
|
|
59
|
+
if (!fragment?.meta) return null;
|
|
60
|
+
const { name, category, tags } = fragment.meta;
|
|
61
61
|
|
|
62
62
|
const nameMatch = fuzzyMatch(name, query);
|
|
63
63
|
if (nameMatch) {
|
|
@@ -119,15 +119,15 @@ function HighlightedText({ text, indices }: { text: string; indices: number[] })
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
interface LeftSidebarProps {
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
fragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
123
|
+
activeFragment: string | null;
|
|
124
124
|
searchQuery: string;
|
|
125
125
|
onSelect: (path: string) => void;
|
|
126
126
|
showHealth?: boolean;
|
|
127
127
|
onHealthClick?: () => void;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
export function LeftSidebar({
|
|
130
|
+
export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect, showHealth, onHealthClick }: LeftSidebarProps) {
|
|
131
131
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
132
132
|
const { setTheme, resolvedTheme } = useTheme();
|
|
133
133
|
const { isMobile, setOpen } = useSidebar();
|
|
@@ -139,14 +139,14 @@ export function LeftSidebar({ segments, activeSegment, searchQuery, onSelect, sh
|
|
|
139
139
|
if (!debouncedSearch) return null;
|
|
140
140
|
|
|
141
141
|
const results: SearchResult[] = [];
|
|
142
|
-
for (const item of
|
|
143
|
-
const result =
|
|
142
|
+
for (const item of fragments) {
|
|
143
|
+
const result = searchFragment(item, debouncedSearch);
|
|
144
144
|
if (result) results.push(result);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
results.sort((a, b) => b.score - a.score);
|
|
148
148
|
return results;
|
|
149
|
-
}, [
|
|
149
|
+
}, [fragments, debouncedSearch]);
|
|
150
150
|
|
|
151
151
|
const highlightMap = useMemo(() => {
|
|
152
152
|
const map = new Map<string, number[]>();
|
|
@@ -161,29 +161,29 @@ export function LeftSidebar({ segments, activeSegment, searchQuery, onSelect, sh
|
|
|
161
161
|
const grouped = useMemo(() => {
|
|
162
162
|
const source = searchResults
|
|
163
163
|
? searchResults.map(r => r.item)
|
|
164
|
-
:
|
|
164
|
+
: fragments;
|
|
165
165
|
|
|
166
|
-
const groups: Record<string, typeof
|
|
166
|
+
const groups: Record<string, typeof fragments> = {};
|
|
167
167
|
for (const item of source) {
|
|
168
|
-
// Skip invalid
|
|
169
|
-
if (!item.
|
|
170
|
-
const category = item.
|
|
168
|
+
// Skip invalid fragments
|
|
169
|
+
if (!item.fragment?.meta) continue;
|
|
170
|
+
const category = item.fragment.meta.category || 'uncategorized';
|
|
171
171
|
if (!groups[category]) groups[category] = [];
|
|
172
172
|
groups[category].push(item);
|
|
173
173
|
}
|
|
174
174
|
return groups;
|
|
175
|
-
}, [
|
|
175
|
+
}, [fragments, searchResults]);
|
|
176
176
|
|
|
177
177
|
const flatItems = useMemo(() => {
|
|
178
|
-
const items: Array<{ path: string;
|
|
178
|
+
const items: Array<{ path: string; fragment: FragmentDefinition }> = [];
|
|
179
179
|
const sortedEntries = Object.entries(grouped).sort(([a], [b]) =>
|
|
180
180
|
a.toLowerCase().localeCompare(b.toLowerCase())
|
|
181
181
|
);
|
|
182
182
|
for (const [, categoryItems] of sortedEntries) {
|
|
183
183
|
const sorted = [...categoryItems]
|
|
184
|
-
.filter(item => item.
|
|
184
|
+
.filter(item => item.fragment?.meta?.name)
|
|
185
185
|
.sort((a, b) =>
|
|
186
|
-
a.
|
|
186
|
+
a.fragment.meta.name.toLowerCase().localeCompare(b.fragment.meta.name.toLowerCase())
|
|
187
187
|
);
|
|
188
188
|
items.push(...sorted);
|
|
189
189
|
}
|
|
@@ -295,9 +295,9 @@ export function LeftSidebar({ segments, activeSegment, searchQuery, onSelect, sh
|
|
|
295
295
|
|
|
296
296
|
{sortedEntries.map(([category, items]) => {
|
|
297
297
|
const sortedItems = [...items]
|
|
298
|
-
.filter(item => item.
|
|
298
|
+
.filter(item => item.fragment?.meta?.name)
|
|
299
299
|
.sort((a, b) =>
|
|
300
|
-
a.
|
|
300
|
+
a.fragment.meta.name.toLowerCase().localeCompare(b.fragment.meta.name.toLowerCase())
|
|
301
301
|
);
|
|
302
302
|
|
|
303
303
|
return (
|
|
@@ -308,11 +308,11 @@ export function LeftSidebar({ segments, activeSegment, searchQuery, onSelect, sh
|
|
|
308
308
|
return (
|
|
309
309
|
<Sidebar.Item
|
|
310
310
|
key={item.path}
|
|
311
|
-
active={
|
|
311
|
+
active={activeFragment === item.path}
|
|
312
312
|
onClick={() => handleSelect(item.path)}
|
|
313
313
|
>
|
|
314
314
|
<HighlightedText
|
|
315
|
-
text={item.
|
|
315
|
+
text={item.fragment.meta.name}
|
|
316
316
|
indices={nameIndices}
|
|
317
317
|
/>
|
|
318
318
|
</Sidebar.Item>
|
|
@@ -332,7 +332,7 @@ export function LeftSidebar({ segments, activeSegment, searchQuery, onSelect, sh
|
|
|
332
332
|
|
|
333
333
|
{/* Footer */}
|
|
334
334
|
<Sidebar.Footer>
|
|
335
|
-
<Badge size="sm">{
|
|
335
|
+
<Badge size="sm">{fragments.length} components</Badge>
|
|
336
336
|
</Sidebar.Footer>
|
|
337
337
|
</>
|
|
338
338
|
);
|