@fragments-sdk/viewer 0.2.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/LICENSE +84 -0
- package/index.html +28 -0
- package/package.json +71 -0
- package/src/__tests__/a11y-fixes.test.ts +358 -0
- package/src/__tests__/jsx-parser.test.ts +502 -0
- package/src/__tests__/render-utils.test.ts +232 -0
- package/src/__tests__/style-utils.test.ts +404 -0
- package/src/app/index.ts +1 -0
- package/src/assets/fragments-logo.ts +4 -0
- package/src/assets/fragments_logo.png +0 -0
- package/src/components/AccessibilityPanel.tsx +1457 -0
- package/src/components/ActionCapture.tsx +172 -0
- package/src/components/ActionsPanel.tsx +332 -0
- package/src/components/AllVariantsPreview.tsx +78 -0
- package/src/components/App.tsx +604 -0
- package/src/components/BottomPanel.tsx +288 -0
- package/src/components/CodePanel.naming.test.tsx +59 -0
- package/src/components/CodePanel.tsx +118 -0
- package/src/components/CommandPalette.tsx +392 -0
- package/src/components/ComponentDocView.tsx +164 -0
- package/src/components/ComponentGraph.tsx +380 -0
- package/src/components/ComponentHeader.tsx +88 -0
- package/src/components/ContractPanel.tsx +241 -0
- package/src/components/DeviceMockup.tsx +156 -0
- package/src/components/EmptyVariantMessage.tsx +54 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/FigmaEmbed.tsx +238 -0
- package/src/components/FragmentEditor.tsx +525 -0
- package/src/components/FragmentRenderer.tsx +61 -0
- package/src/components/HeaderSearch.tsx +24 -0
- package/src/components/HealthDashboard.tsx +441 -0
- package/src/components/HmrStatusIndicator.tsx +61 -0
- package/src/components/Icons.tsx +479 -0
- package/src/components/InteractionsPanel.tsx +757 -0
- package/src/components/IsolatedPreviewFrame.tsx +390 -0
- package/src/components/IsolatedRender.tsx +113 -0
- package/src/components/KeyboardShortcutsHelp.tsx +53 -0
- package/src/components/LandingPage.tsx +420 -0
- package/src/components/Layout.tsx +27 -0
- package/src/components/LeftSidebar.tsx +472 -0
- package/src/components/LoadErrorMessage.tsx +102 -0
- package/src/components/MultiViewportPreview.tsx +527 -0
- package/src/components/NoVariantsMessage.tsx +59 -0
- package/src/components/PanelShell.tsx +161 -0
- package/src/components/PerformancePanel.tsx +304 -0
- package/src/components/PreviewArea.tsx +254 -0
- package/src/components/PreviewAside.tsx +168 -0
- package/src/components/PreviewFrameHost.tsx +304 -0
- package/src/components/PreviewToolbar.tsx +80 -0
- package/src/components/PropsEditor.tsx +506 -0
- package/src/components/PropsTable.tsx +111 -0
- package/src/components/RelationsSection.tsx +88 -0
- package/src/components/ResizablePanel.tsx +271 -0
- package/src/components/RightSidebar.tsx +102 -0
- package/src/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/components/ScreenshotButton.tsx +90 -0
- package/src/components/ShadowPreview.tsx +204 -0
- package/src/components/Sidebar.tsx +169 -0
- package/src/components/SkeletonLoader.tsx +161 -0
- package/src/components/ThemeProvider.tsx +42 -0
- package/src/components/Toast.tsx +3 -0
- package/src/components/TokenStylePanel.tsx +699 -0
- package/src/components/TopToolbar.tsx +159 -0
- package/src/components/Untitled +1 -0
- package/src/components/UsageSection.tsx +95 -0
- package/src/components/VariantMatrix.tsx +391 -0
- package/src/components/VariantRenderer.tsx +131 -0
- package/src/components/VariantTabs.tsx +40 -0
- package/src/components/ViewerHeader.tsx +69 -0
- package/src/components/ViewerStateSync.tsx +52 -0
- package/src/components/ViewportSelector.tsx +172 -0
- package/src/components/WebMCPDevTools.tsx +503 -0
- package/src/components/WebMCPIntegration.tsx +47 -0
- package/src/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/components/_future/CreatePage.tsx +835 -0
- package/src/components/viewer-utils.ts +16 -0
- package/src/composition-renderer.ts +381 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/ui.ts +166 -0
- package/src/entry.tsx +335 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useA11yCache.ts +383 -0
- package/src/hooks/useA11yService.ts +364 -0
- package/src/hooks/useActions.ts +138 -0
- package/src/hooks/useAppState.ts +147 -0
- package/src/hooks/useCompiledFragments.ts +42 -0
- package/src/hooks/useFigmaIntegration.ts +132 -0
- package/src/hooks/useHmrStatus.ts +109 -0
- package/src/hooks/useKeyboardShortcuts.ts +270 -0
- package/src/hooks/usePreviewBridge.ts +347 -0
- package/src/hooks/useScrollSpy.ts +78 -0
- package/src/hooks/useShadowStyles.ts +221 -0
- package/src/hooks/useUrlState.ts +318 -0
- package/src/hooks/useViewSettings.ts +111 -0
- package/src/intelligence/healthReport.ts +505 -0
- package/src/intelligence/styleDrift.ts +340 -0
- package/src/intelligence/usageScanner.ts +309 -0
- package/src/jsx-parser.ts +486 -0
- package/src/preview-frame-entry.tsx +25 -0
- package/src/preview-frame.html +148 -0
- package/src/render-template.html +68 -0
- package/src/render-utils.ts +311 -0
- package/src/shared/ComponentDocContent.module.scss +10 -0
- package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/shared/ComponentDocContent.tsx +274 -0
- package/src/shared/DocsHeaderBar.tsx +129 -0
- package/src/shared/DocsPageAsideHost.tsx +89 -0
- package/src/shared/DocsPageShell.tsx +124 -0
- package/src/shared/DocsSearchCommand.tsx +99 -0
- package/src/shared/DocsSidebarNav.tsx +66 -0
- package/src/shared/PropsTable.module.scss +68 -0
- package/src/shared/PropsTable.module.scss.d.ts +2 -0
- package/src/shared/PropsTable.tsx +76 -0
- package/src/shared/VariantPreviewCard.module.scss +114 -0
- package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/shared/VariantPreviewCard.tsx +137 -0
- package/src/shared/docs-data/index.ts +32 -0
- package/src/shared/docs-data/mcp-configs.ts +72 -0
- package/src/shared/docs-data/palettes.ts +75 -0
- package/src/shared/docs-data/setup-examples.ts +55 -0
- package/src/shared/docs-layout.scss +28 -0
- package/src/shared/docs-layout.scss.d.ts +2 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/types.ts +53 -0
- package/src/style-utils.ts +414 -0
- package/src/styles/globals.css +278 -0
- package/src/types/a11y.ts +197 -0
- package/src/utils/a11y-fixes.ts +509 -0
- package/src/utils/actionExport.ts +372 -0
- package/src/utils/colorSchemes.ts +201 -0
- package/src/utils/contrast.ts +246 -0
- package/src/utils/detectRelationships.ts +256 -0
- package/src/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/webmcp/analytics.ts +165 -0
- package/src/webmcp/index.ts +3 -0
- package/src/webmcp/posthog-bridge.ts +39 -0
- package/src/webmcp/runtime-tools.ts +152 -0
- package/src/webmcp/scan-utils.ts +135 -0
- package/src/webmcp/use-tool-analytics.ts +69 -0
- package/src/webmcp/viewer-state.ts +45 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Graph Panel - Visualizes component relationships
|
|
3
|
+
*
|
|
4
|
+
* Displays a visual graph showing:
|
|
5
|
+
* - The current component in the center
|
|
6
|
+
* - Related components (parent, child, sibling, alternative, composition)
|
|
7
|
+
* - Auto-detected relationships from render analysis
|
|
8
|
+
* - Click to navigate to related components
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
12
|
+
import type { FragmentDefinition, ComponentRelation, RelationshipType } from '@fragments-sdk/core';
|
|
13
|
+
import { ChevronRightIcon, WandIcon } from "./Icons.js";
|
|
14
|
+
import { detectAllRelationships, mergeRelationships } from "../utils/detectRelationships.js";
|
|
15
|
+
import { Stack, Text, Badge, Button, CodeBlock, Separator, Box } from "@fragments-sdk/ui";
|
|
16
|
+
import { TreeStructure } from "@phosphor-icons/react";
|
|
17
|
+
import { PanelShell } from "./PanelShell.js";
|
|
18
|
+
|
|
19
|
+
interface ComponentGraphProps {
|
|
20
|
+
/** Current fragment definition */
|
|
21
|
+
fragment: FragmentDefinition | null;
|
|
22
|
+
/** All available fragments for navigation */
|
|
23
|
+
allFragments: Array<{ path: string; fragment: FragmentDefinition }>;
|
|
24
|
+
/** Callback when a component is clicked */
|
|
25
|
+
onNavigate?: (componentName: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const RELATIONSHIP_CONFIG: Record<RelationshipType, { label: string; color: string; bgColor: string; icon: string }> = {
|
|
29
|
+
parent: {
|
|
30
|
+
label: "Parent",
|
|
31
|
+
color: "#9333ea",
|
|
32
|
+
bgColor: "rgba(147, 51, 234, 0.1)",
|
|
33
|
+
icon: "^",
|
|
34
|
+
},
|
|
35
|
+
child: {
|
|
36
|
+
label: "Child",
|
|
37
|
+
color: "#2563eb",
|
|
38
|
+
bgColor: "rgba(37, 99, 235, 0.1)",
|
|
39
|
+
icon: "v",
|
|
40
|
+
},
|
|
41
|
+
sibling: {
|
|
42
|
+
label: "Sibling",
|
|
43
|
+
color: "#16a34a",
|
|
44
|
+
bgColor: "rgba(22, 163, 74, 0.1)",
|
|
45
|
+
icon: "<->",
|
|
46
|
+
},
|
|
47
|
+
alternative: {
|
|
48
|
+
label: "Alternative",
|
|
49
|
+
color: "#d97706",
|
|
50
|
+
bgColor: "rgba(217, 119, 6, 0.1)",
|
|
51
|
+
icon: "~",
|
|
52
|
+
},
|
|
53
|
+
composition: {
|
|
54
|
+
label: "Composition",
|
|
55
|
+
color: "#0891b2",
|
|
56
|
+
bgColor: "rgba(8, 145, 178, 0.1)",
|
|
57
|
+
icon: "+",
|
|
58
|
+
},
|
|
59
|
+
complementary: {
|
|
60
|
+
label: "Complementary",
|
|
61
|
+
color: "#e11d48",
|
|
62
|
+
bgColor: "rgba(225, 29, 72, 0.1)",
|
|
63
|
+
icon: "&",
|
|
64
|
+
},
|
|
65
|
+
"used-by": {
|
|
66
|
+
label: "Used By",
|
|
67
|
+
color: "#4f46e5",
|
|
68
|
+
bgColor: "rgba(79, 70, 229, 0.1)",
|
|
69
|
+
icon: "<-",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function ComponentGraph({ fragment, allFragments, onNavigate }: ComponentGraphProps) {
|
|
74
|
+
const [showAutoDetected, setShowAutoDetected] = useState(true);
|
|
75
|
+
|
|
76
|
+
// Auto-detect relationships
|
|
77
|
+
const detectedRelationships = useMemo(() => {
|
|
78
|
+
if (!fragment) return [];
|
|
79
|
+
try {
|
|
80
|
+
return detectAllRelationships(fragment, allFragments);
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}, [fragment, allFragments]);
|
|
85
|
+
|
|
86
|
+
// Merge manual and detected relationships
|
|
87
|
+
const allRelationships = useMemo(() => {
|
|
88
|
+
if (!fragment) return [];
|
|
89
|
+
return mergeRelationships(fragment.relations, showAutoDetected ? detectedRelationships : []);
|
|
90
|
+
}, [fragment, detectedRelationships, showAutoDetected]);
|
|
91
|
+
|
|
92
|
+
// Group relations by type
|
|
93
|
+
const groupedRelations = useMemo(() => {
|
|
94
|
+
if (allRelationships.length === 0) return null;
|
|
95
|
+
|
|
96
|
+
const groups: Record<RelationshipType, Array<ComponentRelation & { isDetected?: boolean; confidence?: number }>> = {
|
|
97
|
+
parent: [],
|
|
98
|
+
child: [],
|
|
99
|
+
sibling: [],
|
|
100
|
+
alternative: [],
|
|
101
|
+
composition: [],
|
|
102
|
+
complementary: [],
|
|
103
|
+
"used-by": [],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (const relation of allRelationships) {
|
|
107
|
+
groups[relation.relationship].push(relation);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return groups;
|
|
111
|
+
}, [allRelationships]);
|
|
112
|
+
|
|
113
|
+
// Find reverse relations (components that reference this one)
|
|
114
|
+
const reverseRelations = useMemo(() => {
|
|
115
|
+
if (!fragment) return [];
|
|
116
|
+
|
|
117
|
+
const componentName = fragment.meta.name;
|
|
118
|
+
const reverse: Array<{ fragment: FragmentDefinition; relation: ComponentRelation }> = [];
|
|
119
|
+
|
|
120
|
+
for (const { fragment: otherFragment } of allFragments) {
|
|
121
|
+
if (otherFragment.meta.name === componentName) continue;
|
|
122
|
+
if (!otherFragment.relations) continue;
|
|
123
|
+
|
|
124
|
+
for (const relation of otherFragment.relations) {
|
|
125
|
+
if (relation.component === componentName) {
|
|
126
|
+
reverse.push({
|
|
127
|
+
fragment: otherFragment,
|
|
128
|
+
relation: {
|
|
129
|
+
...relation,
|
|
130
|
+
component: otherFragment.meta.name,
|
|
131
|
+
// Flip the relationship direction for display
|
|
132
|
+
relationship: flipRelationship(relation.relationship),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return reverse;
|
|
140
|
+
}, [fragment, allFragments]);
|
|
141
|
+
|
|
142
|
+
// Check if a component exists in our fragments
|
|
143
|
+
const componentExists = (name: string) => {
|
|
144
|
+
return allFragments.some(s => s.fragment.meta.name === name);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleNavigate = (componentName: string) => {
|
|
148
|
+
if (componentExists(componentName) && onNavigate) {
|
|
149
|
+
onNavigate(componentName);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const hasRelations = (groupedRelations && Object.values(groupedRelations).some(g => g.length > 0)) || reverseRelations.length > 0;
|
|
154
|
+
const detectedCount = detectedRelationships.length;
|
|
155
|
+
|
|
156
|
+
// Determine empty config
|
|
157
|
+
const emptyConfig = !fragment ? {
|
|
158
|
+
icon: <TreeStructure size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
|
|
159
|
+
title: "No component selected",
|
|
160
|
+
description: "Select a component to view its relationships",
|
|
161
|
+
} : !hasRelations ? {
|
|
162
|
+
icon: <TreeStructure size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
|
|
163
|
+
title: "No relationships defined",
|
|
164
|
+
description: (
|
|
165
|
+
<>
|
|
166
|
+
Add a <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline' }}>relations</Box> array to your fragment definition to visualize component connections.
|
|
167
|
+
</>
|
|
168
|
+
),
|
|
169
|
+
action: (
|
|
170
|
+
<CodeBlock language="typescript" code={`relations: [
|
|
171
|
+
{
|
|
172
|
+
component: "ButtonGroup",
|
|
173
|
+
relationship: "parent",
|
|
174
|
+
note: "Use ButtonGroup for multiple buttons"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
component: "IconButton",
|
|
178
|
+
relationship: "alternative",
|
|
179
|
+
note: "Use when only an icon is needed"
|
|
180
|
+
}
|
|
181
|
+
]`} />
|
|
182
|
+
),
|
|
183
|
+
} : undefined;
|
|
184
|
+
|
|
185
|
+
// Toolbar with badges + auto-detect toggle (only when we have data)
|
|
186
|
+
const toolbar = fragment && hasRelations ? (
|
|
187
|
+
<Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
|
|
188
|
+
<Stack direction="row" align="center" gap="sm">
|
|
189
|
+
<Badge size="sm">{fragment.meta.name}</Badge>
|
|
190
|
+
<Badge size="sm" variant="default">
|
|
191
|
+
{allRelationships.length + reverseRelations.length} connection{(allRelationships.length + reverseRelations.length) !== 1 ? 's' : ''}
|
|
192
|
+
</Badge>
|
|
193
|
+
</Stack>
|
|
194
|
+
{detectedCount > 0 && (
|
|
195
|
+
<AutoDetectToggle
|
|
196
|
+
showAutoDetected={showAutoDetected}
|
|
197
|
+
onToggle={() => setShowAutoDetected(!showAutoDetected)}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</Stack>
|
|
201
|
+
) : undefined;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<PanelShell toolbar={toolbar} empty={emptyConfig}>
|
|
205
|
+
{/* Defined relations */}
|
|
206
|
+
{groupedRelations && (
|
|
207
|
+
<Stack direction="column" gap="md">
|
|
208
|
+
{(Object.entries(groupedRelations) as [RelationshipType, ComponentRelation[]][]).map(
|
|
209
|
+
([type, relations]) => {
|
|
210
|
+
if (relations.length === 0) return null;
|
|
211
|
+
const config = RELATIONSHIP_CONFIG[type];
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div key={type}>
|
|
215
|
+
<Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
|
|
216
|
+
<Text size="xs" weight="medium" style={{ textTransform: 'uppercase', letterSpacing: '0.05em', color: config.color }}>
|
|
217
|
+
{config.label}
|
|
218
|
+
</Text>
|
|
219
|
+
<Separator style={{ flex: 1 }} />
|
|
220
|
+
</Stack>
|
|
221
|
+
<Stack direction="column" gap="sm">
|
|
222
|
+
{relations.map((relation, index) => (
|
|
223
|
+
<RelationCard
|
|
224
|
+
key={`${relation.component}-${index}`}
|
|
225
|
+
relation={relation}
|
|
226
|
+
config={config}
|
|
227
|
+
exists={componentExists(relation.component)}
|
|
228
|
+
onNavigate={handleNavigate}
|
|
229
|
+
/>
|
|
230
|
+
))}
|
|
231
|
+
</Stack>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
)}
|
|
236
|
+
</Stack>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* Reverse relations (inbound references) */}
|
|
240
|
+
{reverseRelations.length > 0 && (
|
|
241
|
+
<Box style={{ marginTop: '24px' }}>
|
|
242
|
+
<Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
|
|
243
|
+
<Text size="xs" weight="medium" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
244
|
+
Referenced By
|
|
245
|
+
</Text>
|
|
246
|
+
<Separator style={{ flex: 1 }} />
|
|
247
|
+
</Stack>
|
|
248
|
+
<Stack direction="column" gap="sm">
|
|
249
|
+
{reverseRelations.map(({ relation }, index) => {
|
|
250
|
+
const config = RELATIONSHIP_CONFIG[relation.relationship];
|
|
251
|
+
return (
|
|
252
|
+
<RelationCard
|
|
253
|
+
key={`reverse-${relation.component}-${index}`}
|
|
254
|
+
relation={relation}
|
|
255
|
+
config={config}
|
|
256
|
+
exists={componentExists(relation.component)}
|
|
257
|
+
onNavigate={handleNavigate}
|
|
258
|
+
isReverse
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
</Stack>
|
|
263
|
+
</Box>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Legend */}
|
|
267
|
+
<Box borderTop style={{ marginTop: '24px', paddingTop: '12px' }}>
|
|
268
|
+
<Stack direction="row" gap="md" style={{ flexWrap: 'wrap' }}>
|
|
269
|
+
{(Object.entries(RELATIONSHIP_CONFIG) as [RelationshipType, typeof RELATIONSHIP_CONFIG[RelationshipType]][]).map(
|
|
270
|
+
([type, config]) => (
|
|
271
|
+
<Stack key={type} direction="row" align="center" gap="xs">
|
|
272
|
+
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: config.color, flexShrink: 0 }} />
|
|
273
|
+
<Text size="xs" color="tertiary">{config.label}</Text>
|
|
274
|
+
</Stack>
|
|
275
|
+
)
|
|
276
|
+
)}
|
|
277
|
+
</Stack>
|
|
278
|
+
</Box>
|
|
279
|
+
</PanelShell>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function AutoDetectToggle({ showAutoDetected, onToggle }: { showAutoDetected: boolean; onToggle: () => void }) {
|
|
284
|
+
return (
|
|
285
|
+
<Button
|
|
286
|
+
onClick={onToggle}
|
|
287
|
+
variant="ghost"
|
|
288
|
+
size="sm"
|
|
289
|
+
style={{
|
|
290
|
+
background: showAutoDetected ? 'rgba(147, 51, 234, 0.1)' : 'transparent',
|
|
291
|
+
color: showAutoDetected ? '#9333ea' : undefined,
|
|
292
|
+
}}
|
|
293
|
+
title={showAutoDetected ? "Hide composition relationships detected from variants" : "Show composition relationships detected from variants"}
|
|
294
|
+
>
|
|
295
|
+
<WandIcon style={{ width: '14px', height: '14px' }} />
|
|
296
|
+
{showAutoDetected ? "Composition on" : "Composition off"}
|
|
297
|
+
</Button>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface RelationCardProps {
|
|
302
|
+
relation: ComponentRelation & { isDetected?: boolean; confidence?: number };
|
|
303
|
+
config: typeof RELATIONSHIP_CONFIG[RelationshipType];
|
|
304
|
+
exists: boolean;
|
|
305
|
+
onNavigate: (name: string) => void;
|
|
306
|
+
isReverse?: boolean;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function RelationCard({ relation, config, exists, onNavigate, isReverse }: RelationCardProps) {
|
|
310
|
+
const [hovered, setHovered] = useState(false);
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => exists && onNavigate(relation.component)}
|
|
315
|
+
disabled={!exists}
|
|
316
|
+
onMouseEnter={() => setHovered(true)}
|
|
317
|
+
onMouseLeave={() => setHovered(false)}
|
|
318
|
+
style={{
|
|
319
|
+
width: '100%',
|
|
320
|
+
textAlign: 'left',
|
|
321
|
+
padding: '12px',
|
|
322
|
+
borderRadius: '8px',
|
|
323
|
+
border: exists ? '1px solid transparent' : '1px dashed var(--border)',
|
|
324
|
+
transition: 'transform 150ms, box-shadow 150ms',
|
|
325
|
+
background: config.bgColor,
|
|
326
|
+
cursor: exists ? 'pointer' : 'not-allowed',
|
|
327
|
+
opacity: exists ? 1 : 0.6,
|
|
328
|
+
transform: exists && hovered ? 'scale(1.01)' : 'scale(1)',
|
|
329
|
+
boxShadow: exists && hovered ? '0 4px 6px -1px rgba(0,0,0,0.1)' : 'none',
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<Stack direction="row" align="center" justify="between">
|
|
333
|
+
<Stack direction="row" align="center" gap="sm" style={{ flexWrap: 'wrap' }}>
|
|
334
|
+
<Text weight="medium" size="sm" style={{ color: config.color }}>
|
|
335
|
+
{relation.component}
|
|
336
|
+
</Text>
|
|
337
|
+
{relation.isDetected && (
|
|
338
|
+
<Badge size="sm" style={{ background: 'rgba(147, 51, 234, 0.1)', color: '#9333ea' }}>
|
|
339
|
+
<WandIcon style={{ width: '10px', height: '10px' }} />
|
|
340
|
+
auto
|
|
341
|
+
</Badge>
|
|
342
|
+
)}
|
|
343
|
+
{isReverse && (
|
|
344
|
+
<Badge size="sm" variant="default">inbound</Badge>
|
|
345
|
+
)}
|
|
346
|
+
{!exists && (
|
|
347
|
+
<Text size="xs" color="secondary" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
|
348
|
+
(not found)
|
|
349
|
+
</Text>
|
|
350
|
+
)}
|
|
351
|
+
</Stack>
|
|
352
|
+
{exists && (
|
|
353
|
+
<ChevronRightIcon style={{ width: '16px', height: '16px', color: 'var(--text-tertiary)' }} />
|
|
354
|
+
)}
|
|
355
|
+
</Stack>
|
|
356
|
+
{relation.note && (
|
|
357
|
+
<Text size="xs" color="secondary" style={{ marginTop: '4px' }}>
|
|
358
|
+
{relation.note}
|
|
359
|
+
</Text>
|
|
360
|
+
)}
|
|
361
|
+
</button>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Flip a relationship for reverse display.
|
|
367
|
+
* e.g., if Button has child: IconButton, then IconButton sees Button as parent
|
|
368
|
+
*/
|
|
369
|
+
function flipRelationship(type: RelationshipType): RelationshipType {
|
|
370
|
+
switch (type) {
|
|
371
|
+
case "parent":
|
|
372
|
+
return "child";
|
|
373
|
+
case "child":
|
|
374
|
+
return "parent";
|
|
375
|
+
case "used-by":
|
|
376
|
+
return "composition";
|
|
377
|
+
default:
|
|
378
|
+
return type;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { FragmentMeta } from '@fragments-sdk/core';
|
|
2
|
+
import { Badge, Alert, Text, Stack, Separator } from '@fragments-sdk/ui';
|
|
3
|
+
import { WarningIcon, BeakerIcon } from './Icons.js';
|
|
4
|
+
|
|
5
|
+
interface ComponentHeaderProps {
|
|
6
|
+
meta: FragmentMeta;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Map status to Badge variant
|
|
10
|
+
function getStatusBadgeVariant(status: string): 'success' | 'warning' | 'error' | 'default' {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case 'stable': return 'success';
|
|
13
|
+
case 'experimental': return 'warning';
|
|
14
|
+
case 'deprecated': return 'error';
|
|
15
|
+
case 'beta': return 'warning';
|
|
16
|
+
default: return 'default';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ComponentHeader({ meta }: ComponentHeaderProps) {
|
|
21
|
+
return (
|
|
22
|
+
<section id="overview" style={{ scrollMarginTop: '96px', marginBottom: '24px' }}>
|
|
23
|
+
{/* Deprecated Warning Banner */}
|
|
24
|
+
{meta.status === 'deprecated' && (
|
|
25
|
+
<div style={{ marginBottom: '20px' }}>
|
|
26
|
+
<Alert severity="error">
|
|
27
|
+
<Alert.Icon><WarningIcon /></Alert.Icon>
|
|
28
|
+
<Alert.Body>
|
|
29
|
+
<Alert.Title>Deprecated Component</Alert.Title>
|
|
30
|
+
<Alert.Content>
|
|
31
|
+
This component is deprecated and will be removed in a future version.
|
|
32
|
+
{meta.tags?.includes('migration') && ' Check the usage guidelines for migration instructions.'}
|
|
33
|
+
</Alert.Content>
|
|
34
|
+
</Alert.Body>
|
|
35
|
+
</Alert>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{/* Experimental Warning Banner */}
|
|
40
|
+
{meta.status === 'experimental' && (
|
|
41
|
+
<div style={{ marginBottom: '20px' }}>
|
|
42
|
+
<Alert severity="warning">
|
|
43
|
+
<Alert.Icon><BeakerIcon /></Alert.Icon>
|
|
44
|
+
<Alert.Body>
|
|
45
|
+
<Alert.Title>Experimental Component</Alert.Title>
|
|
46
|
+
<Alert.Content>
|
|
47
|
+
This component is experimental. The API may change without notice.
|
|
48
|
+
</Alert.Content>
|
|
49
|
+
</Alert.Body>
|
|
50
|
+
</Alert>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<Text as="h1" size="xl" weight="semibold" style={{ letterSpacing: '-0.025em', marginBottom: '8px' }}>
|
|
55
|
+
{meta.name}
|
|
56
|
+
</Text>
|
|
57
|
+
<Text as="p" size="base" color="secondary" style={{ lineHeight: 1.625, marginBottom: '20px' }}>
|
|
58
|
+
{meta.description}
|
|
59
|
+
</Text>
|
|
60
|
+
|
|
61
|
+
<Stack direction="row" gap="sm" wrap align="center">
|
|
62
|
+
{/* Category badge */}
|
|
63
|
+
{meta.category && (
|
|
64
|
+
<Badge variant="default">{meta.category}</Badge>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{/* Status badge */}
|
|
68
|
+
{meta.status && (
|
|
69
|
+
<Badge variant={getStatusBadgeVariant(meta.status)}>
|
|
70
|
+
{meta.status}
|
|
71
|
+
</Badge>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Tags */}
|
|
75
|
+
{meta.tags && meta.tags.length > 0 && (
|
|
76
|
+
<>
|
|
77
|
+
<Separator orientation="vertical" />
|
|
78
|
+
{meta.tags.map((tag) => (
|
|
79
|
+
<Badge key={tag} variant="default">
|
|
80
|
+
{tag}
|
|
81
|
+
</Badge>
|
|
82
|
+
))}
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
</Stack>
|
|
86
|
+
</section>
|
|
87
|
+
);
|
|
88
|
+
}
|