@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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContractPanel component - displays FragmentContract metadata for AI agents.
|
|
3
|
+
* Shows propsSummary, scenarioTags, bans, and a11yRules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { memo } from 'react';
|
|
7
|
+
import type { FragmentContract } from '@fragments-sdk/core';
|
|
8
|
+
import { Stack, Text, Card, Chip, Alert, EmptyState, CodeBlock, Badge, Box } from '@fragments-sdk/ui';
|
|
9
|
+
|
|
10
|
+
interface ContractPanelProps {
|
|
11
|
+
contract?: FragmentContract;
|
|
12
|
+
componentName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ContractPanel = memo(function ContractPanel({
|
|
16
|
+
contract,
|
|
17
|
+
componentName,
|
|
18
|
+
}: ContractPanelProps) {
|
|
19
|
+
// Empty state when no contract metadata
|
|
20
|
+
if (!contract || isContractEmpty(contract)) {
|
|
21
|
+
return (
|
|
22
|
+
<EmptyState>
|
|
23
|
+
<EmptyState.Icon>
|
|
24
|
+
<svg
|
|
25
|
+
style={{ width: '48px', height: '48px', opacity: 0.5 }}
|
|
26
|
+
fill="none"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
strokeWidth={1.5}
|
|
34
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
35
|
+
/>
|
|
36
|
+
</svg>
|
|
37
|
+
</EmptyState.Icon>
|
|
38
|
+
<EmptyState.Title>No contract metadata</EmptyState.Title>
|
|
39
|
+
<EmptyState.Description>
|
|
40
|
+
Add a <Box as="code" padding="xs" background="secondary" rounded="sm">contract</Box> field to the fragment definition to enable AI agent features.
|
|
41
|
+
</EmptyState.Description>
|
|
42
|
+
<EmptyState.Actions>
|
|
43
|
+
<CodeBlock
|
|
44
|
+
code={`contract: {
|
|
45
|
+
propsSummary: ['variant: primary|secondary'],
|
|
46
|
+
scenarioTags: ['form.submit', 'action.primary'],
|
|
47
|
+
a11yRules: ['A11Y_BTN_LABEL'],
|
|
48
|
+
}`}
|
|
49
|
+
language="typescript"
|
|
50
|
+
compact
|
|
51
|
+
/>
|
|
52
|
+
</EmptyState.Actions>
|
|
53
|
+
</EmptyState>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Box padding="md" style={{ fontSize: '14px' }}>
|
|
59
|
+
<Stack gap="lg">
|
|
60
|
+
{/* Props Summary */}
|
|
61
|
+
{contract.propsSummary && contract.propsSummary.length > 0 && (
|
|
62
|
+
<Section title="Props Summary">
|
|
63
|
+
<Card>
|
|
64
|
+
<Card.Body>
|
|
65
|
+
<table style={{ width: '100%', fontSize: '12px', borderCollapse: 'collapse' }}>
|
|
66
|
+
<thead>
|
|
67
|
+
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
|
68
|
+
<th style={{ textAlign: 'left', padding: '8px 16px 8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Name</th>
|
|
69
|
+
<th style={{ textAlign: 'left', padding: '8px 16px 8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Type</th>
|
|
70
|
+
<th style={{ textAlign: 'left', padding: '8px 0', fontWeight: 500, color: 'var(--text-tertiary)' }}>Description</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
{contract.propsSummary.map((prop, i) => {
|
|
75
|
+
const parsed = parsePropSummary(prop);
|
|
76
|
+
return (
|
|
77
|
+
<tr key={i} style={{ borderBottom: i < contract.propsSummary!.length - 1 ? '1px solid var(--border)' : undefined }}>
|
|
78
|
+
<td style={{ padding: '8px 16px 8px 0', verticalAlign: 'top' }}>
|
|
79
|
+
<Text font="mono" size="xs" weight="medium">{parsed.name}</Text>
|
|
80
|
+
</td>
|
|
81
|
+
<td style={{ padding: '8px 16px 8px 0', verticalAlign: 'top' }}>
|
|
82
|
+
<Badge size="sm">{parsed.type}</Badge>
|
|
83
|
+
</td>
|
|
84
|
+
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>
|
|
85
|
+
<Text size="xs" color="secondary">{parsed.description}</Text>
|
|
86
|
+
</td>
|
|
87
|
+
</tr>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</Card.Body>
|
|
93
|
+
</Card>
|
|
94
|
+
</Section>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Scenario Tags */}
|
|
98
|
+
{contract.scenarioTags && contract.scenarioTags.length > 0 && (
|
|
99
|
+
<Section title="Scenario Tags" subtitle="Used by fragments_suggest for AI queries">
|
|
100
|
+
<Stack direction="row" gap="sm" style={{ flexWrap: 'wrap' }}>
|
|
101
|
+
{contract.scenarioTags.map((tag, i) => (
|
|
102
|
+
<Chip key={i}>{tag}</Chip>
|
|
103
|
+
))}
|
|
104
|
+
</Stack>
|
|
105
|
+
</Section>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Accessibility Rules */}
|
|
109
|
+
{contract.a11yRules && contract.a11yRules.length > 0 && (
|
|
110
|
+
<Section title="Accessibility Rules">
|
|
111
|
+
<Stack gap="sm">
|
|
112
|
+
{contract.a11yRules.map((rule, i) => (
|
|
113
|
+
<Box key={i} padding="sm" background="secondary" rounded="sm">
|
|
114
|
+
<Stack
|
|
115
|
+
direction="row"
|
|
116
|
+
align="center"
|
|
117
|
+
gap="sm"
|
|
118
|
+
>
|
|
119
|
+
<span style={{ color: '#22c55e' }}>
|
|
120
|
+
<svg style={{ width: '16px', height: '16px' }} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
121
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
122
|
+
</svg>
|
|
123
|
+
</span>
|
|
124
|
+
<Text font="mono" size="xs" color="secondary">{rule}</Text>
|
|
125
|
+
</Stack>
|
|
126
|
+
</Box>
|
|
127
|
+
))}
|
|
128
|
+
</Stack>
|
|
129
|
+
</Section>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Bans */}
|
|
133
|
+
{contract.bans && contract.bans.length > 0 && (
|
|
134
|
+
<Section title="Usage Bans" subtitle="Patterns to avoid">
|
|
135
|
+
<Stack gap="sm">
|
|
136
|
+
{contract.bans.map((ban, i) => (
|
|
137
|
+
<Alert key={i} severity="warning">
|
|
138
|
+
<Alert.Body>
|
|
139
|
+
<Alert.Title>
|
|
140
|
+
<Text font="mono" size="xs">{ban.pattern}</Text>
|
|
141
|
+
</Alert.Title>
|
|
142
|
+
<Alert.Content>
|
|
143
|
+
<Text size="xs" color="tertiary">{ban.message}</Text>
|
|
144
|
+
</Alert.Content>
|
|
145
|
+
</Alert.Body>
|
|
146
|
+
</Alert>
|
|
147
|
+
))}
|
|
148
|
+
</Stack>
|
|
149
|
+
</Section>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{/* Empty sections message */}
|
|
153
|
+
{isPartialContract(contract) && (
|
|
154
|
+
<Alert severity="info">
|
|
155
|
+
<Alert.Body>
|
|
156
|
+
<Alert.Content>
|
|
157
|
+
Tip: Add more contract fields to help AI agents better understand this component.
|
|
158
|
+
</Alert.Content>
|
|
159
|
+
</Alert.Body>
|
|
160
|
+
</Alert>
|
|
161
|
+
)}
|
|
162
|
+
</Stack>
|
|
163
|
+
</Box>
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Helper component for consistent section styling
|
|
168
|
+
interface SectionProps {
|
|
169
|
+
title: string;
|
|
170
|
+
subtitle?: string;
|
|
171
|
+
children: React.ReactNode;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Section({ title, subtitle, children }: SectionProps) {
|
|
175
|
+
return (
|
|
176
|
+
<div>
|
|
177
|
+
<Stack direction="row" align="center" gap="sm" style={{ marginBottom: '8px' }}>
|
|
178
|
+
<Text as="h3" size="xs" weight="semibold">{title}</Text>
|
|
179
|
+
{subtitle && (
|
|
180
|
+
<Text size="xs" color="tertiary">({subtitle})</Text>
|
|
181
|
+
)}
|
|
182
|
+
</Stack>
|
|
183
|
+
{children}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if contract is empty or has no meaningful content
|
|
189
|
+
function isContractEmpty(contract: FragmentContract): boolean {
|
|
190
|
+
return (
|
|
191
|
+
(!contract.propsSummary || contract.propsSummary.length === 0) &&
|
|
192
|
+
(!contract.scenarioTags || contract.scenarioTags.length === 0) &&
|
|
193
|
+
(!contract.a11yRules || contract.a11yRules.length === 0) &&
|
|
194
|
+
(!contract.bans || contract.bans.length === 0)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check if contract has some but not all fields
|
|
199
|
+
function isPartialContract(contract: FragmentContract): boolean {
|
|
200
|
+
const fieldCount = [
|
|
201
|
+
contract.propsSummary?.length ?? 0,
|
|
202
|
+
contract.scenarioTags?.length ?? 0,
|
|
203
|
+
contract.a11yRules?.length ?? 0,
|
|
204
|
+
contract.bans?.length ?? 0,
|
|
205
|
+
].filter(n => n > 0).length;
|
|
206
|
+
|
|
207
|
+
return fieldCount > 0 && fieldCount < 3;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Parse a prop summary string like "name: type - description" or "name: type (default: value)"
|
|
211
|
+
function parsePropSummary(prop: string): { name: string; type: string; description: string } {
|
|
212
|
+
// Try to match "name: type - description" format
|
|
213
|
+
const colonIndex = prop.indexOf(':');
|
|
214
|
+
if (colonIndex === -1) {
|
|
215
|
+
return { name: prop, type: '', description: '' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const name = prop.slice(0, colonIndex).trim();
|
|
219
|
+
const rest = prop.slice(colonIndex + 1).trim();
|
|
220
|
+
|
|
221
|
+
// Check for " - description" pattern
|
|
222
|
+
const dashIndex = rest.indexOf(' - ');
|
|
223
|
+
if (dashIndex !== -1) {
|
|
224
|
+
const type = rest.slice(0, dashIndex).trim();
|
|
225
|
+
const description = rest.slice(dashIndex + 3).trim();
|
|
226
|
+
return { name, type, description };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for "(default: value)" or "(default: value) description" pattern
|
|
230
|
+
const defaultMatch = rest.match(/^([^(]+)(\(default:\s*[^)]+\))(.*)$/);
|
|
231
|
+
if (defaultMatch) {
|
|
232
|
+
const type = defaultMatch[1].trim();
|
|
233
|
+
const defaultInfo = defaultMatch[2];
|
|
234
|
+
const extraDesc = defaultMatch[3].trim();
|
|
235
|
+
const description = extraDesc ? `${defaultInfo} ${extraDesc}` : defaultInfo;
|
|
236
|
+
return { name, type, description };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Just type, no description
|
|
240
|
+
return { name, type: rest, description: '' };
|
|
241
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceMockup - renders children inside a tablet or mobile device frame.
|
|
3
|
+
*
|
|
4
|
+
* Provides physical chrome (bezel, buttons, notch/camera, home indicator)
|
|
5
|
+
* around the content for realistic device preview.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { memo, type ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
interface DeviceMockupProps {
|
|
11
|
+
type: "tablet" | "mobile";
|
|
12
|
+
width: number;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BEZEL_BG = "#1a1a1a";
|
|
17
|
+
const BUTTON_BG = "#2a2a2a";
|
|
18
|
+
|
|
19
|
+
const MOBILE_BUTTONS: Array<{
|
|
20
|
+
side: "left" | "right";
|
|
21
|
+
top: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}> = [
|
|
24
|
+
{ side: "left", top: 96, height: 32 },
|
|
25
|
+
{ side: "left", top: 144, height: 48 },
|
|
26
|
+
{ side: "left", top: 208, height: 48 },
|
|
27
|
+
{ side: "right", top: 128, height: 64 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const DeviceMockup = memo(function DeviceMockup({
|
|
31
|
+
type,
|
|
32
|
+
width,
|
|
33
|
+
children,
|
|
34
|
+
}: DeviceMockupProps) {
|
|
35
|
+
const isMobile = type === "mobile";
|
|
36
|
+
const frameWidth = width + 24;
|
|
37
|
+
const frameHeight = isMobile ? 720 : 1024;
|
|
38
|
+
const screenHeight = frameHeight - (isMobile ? 80 : 48);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div style={{ position: "relative", flexShrink: 0, width: frameWidth }}>
|
|
42
|
+
{/* Bezel */}
|
|
43
|
+
<div
|
|
44
|
+
style={{
|
|
45
|
+
position: "relative",
|
|
46
|
+
borderRadius: 40,
|
|
47
|
+
background: BEZEL_BG,
|
|
48
|
+
padding: 12,
|
|
49
|
+
boxShadow:
|
|
50
|
+
"0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255,255,255,0.1)",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{/* Physical side buttons (mobile only) */}
|
|
54
|
+
{isMobile &&
|
|
55
|
+
MOBILE_BUTTONS.map((btn) => (
|
|
56
|
+
<div
|
|
57
|
+
key={`${btn.side}-${btn.top}`}
|
|
58
|
+
style={{
|
|
59
|
+
position: "absolute",
|
|
60
|
+
[btn.side]: -3,
|
|
61
|
+
top: btn.top,
|
|
62
|
+
width: 3,
|
|
63
|
+
height: btn.height,
|
|
64
|
+
background: BUTTON_BG,
|
|
65
|
+
borderRadius:
|
|
66
|
+
btn.side === "left" ? "4px 0 0 4px" : "0 4px 4px 0",
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
|
|
71
|
+
{/* Screen */}
|
|
72
|
+
<div
|
|
73
|
+
style={{
|
|
74
|
+
position: "relative",
|
|
75
|
+
borderRadius: 32,
|
|
76
|
+
overflow: "hidden",
|
|
77
|
+
background: "white",
|
|
78
|
+
height: screenHeight,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{/* Notch (mobile) or camera dot (tablet) */}
|
|
82
|
+
{isMobile ? (
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
position: "absolute",
|
|
86
|
+
top: 0,
|
|
87
|
+
left: "50%",
|
|
88
|
+
transform: "translateX(-50%)",
|
|
89
|
+
width: 120,
|
|
90
|
+
height: 30,
|
|
91
|
+
background: BEZEL_BG,
|
|
92
|
+
borderRadius: "0 0 16px 16px",
|
|
93
|
+
zIndex: 10,
|
|
94
|
+
display: "flex",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
justifyContent: "center",
|
|
97
|
+
gap: 8,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
style={{
|
|
102
|
+
width: 8,
|
|
103
|
+
height: 8,
|
|
104
|
+
borderRadius: "50%",
|
|
105
|
+
background: BUTTON_BG,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
width: 48,
|
|
111
|
+
height: 6,
|
|
112
|
+
borderRadius: 9999,
|
|
113
|
+
background: BUTTON_BG,
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
<div
|
|
119
|
+
style={{
|
|
120
|
+
position: "absolute",
|
|
121
|
+
top: 8,
|
|
122
|
+
left: "50%",
|
|
123
|
+
transform: "translateX(-50%)",
|
|
124
|
+
width: 12,
|
|
125
|
+
height: 12,
|
|
126
|
+
borderRadius: "50%",
|
|
127
|
+
background: BUTTON_BG,
|
|
128
|
+
zIndex: 10,
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Content area */}
|
|
134
|
+
<div style={{ width: "100%", height: "100%", overflow: "auto" }}>
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Home indicator bar */}
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
position: "absolute",
|
|
142
|
+
bottom: 8,
|
|
143
|
+
left: "50%",
|
|
144
|
+
transform: "translateX(-50%)",
|
|
145
|
+
width: 100,
|
|
146
|
+
height: 4,
|
|
147
|
+
background: "rgba(0, 0, 0, 0.2)",
|
|
148
|
+
borderRadius: 9999,
|
|
149
|
+
zIndex: 10,
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Stack, Text, Alert } from "@fragments-sdk/ui";
|
|
2
|
+
|
|
3
|
+
interface EmptyVariantMessageProps {
|
|
4
|
+
reason: string;
|
|
5
|
+
variantName: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function EmptyVariantMessage({ reason, variantName, hint }: EmptyVariantMessageProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Alert variant="warning">
|
|
12
|
+
<Alert.Body>
|
|
13
|
+
<Alert.Title>Variant "{variantName}" rendered empty</Alert.Title>
|
|
14
|
+
<Alert.Content>
|
|
15
|
+
<Stack direction="column" gap="sm">
|
|
16
|
+
<Text size="xs" color="secondary">
|
|
17
|
+
{reason}
|
|
18
|
+
</Text>
|
|
19
|
+
{hint && (
|
|
20
|
+
<Text size="xs" color="tertiary">
|
|
21
|
+
<Text as="span" size="xs" weight="semibold">
|
|
22
|
+
Tip:
|
|
23
|
+
</Text>{" "}
|
|
24
|
+
{hint}
|
|
25
|
+
</Text>
|
|
26
|
+
)}
|
|
27
|
+
<div>
|
|
28
|
+
<Text size="xs" color="tertiary" weight="semibold">
|
|
29
|
+
Common causes:
|
|
30
|
+
</Text>
|
|
31
|
+
<ul style={{ marginTop: "4px", marginLeft: "16px", listStyleType: "disc" }}>
|
|
32
|
+
<li>
|
|
33
|
+
<Text size="xs" color="secondary">
|
|
34
|
+
Component requires props that weren't provided
|
|
35
|
+
</Text>
|
|
36
|
+
</li>
|
|
37
|
+
<li>
|
|
38
|
+
<Text size="xs" color="secondary">
|
|
39
|
+
Component renders conditionally and conditions aren't met
|
|
40
|
+
</Text>
|
|
41
|
+
</li>
|
|
42
|
+
<li>
|
|
43
|
+
<Text size="xs" color="secondary">
|
|
44
|
+
Story args reference variables that don't exist in this context
|
|
45
|
+
</Text>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</div>
|
|
49
|
+
</Stack>
|
|
50
|
+
</Alert.Content>
|
|
51
|
+
</Alert.Body>
|
|
52
|
+
</Alert>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Component, type ReactNode, type ErrorInfo } from 'react';
|
|
2
|
+
import { Button, Alert, CodeBlock, Collapsible, Stack, Text } from '@fragments-sdk/ui';
|
|
3
|
+
import { ErrorIcon, RefreshIcon } from './Icons.js';
|
|
4
|
+
|
|
5
|
+
interface ErrorBoundaryProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
componentName?: string;
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ErrorBoundaryState {
|
|
13
|
+
hasError: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
errorInfo: ErrorInfo | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
19
|
+
constructor(props: ErrorBoundaryProps) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = { hasError: false, error: null, errorInfo: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
|
|
25
|
+
return { hasError: true, error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
29
|
+
console.error('Component Error:', error);
|
|
30
|
+
console.error('Component Stack:', errorInfo.componentStack);
|
|
31
|
+
this.setState({ errorInfo });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
handleRetry = () => {
|
|
35
|
+
this.setState({ hasError: false, error: null, errorInfo: null });
|
|
36
|
+
this.props.onRetry?.();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
if (this.state.hasError) {
|
|
41
|
+
if (this.props.fallback) {
|
|
42
|
+
return this.props.fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { componentName } = this.props;
|
|
46
|
+
const { error, errorInfo } = this.state;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Alert variant="error">
|
|
50
|
+
<Alert.Icon>
|
|
51
|
+
<ErrorIcon style={{ width: '24px', height: '24px' }} />
|
|
52
|
+
</Alert.Icon>
|
|
53
|
+
<Alert.Body>
|
|
54
|
+
<Alert.Title>
|
|
55
|
+
{componentName ? `Error rendering ${componentName}` : 'Component Error'}
|
|
56
|
+
</Alert.Title>
|
|
57
|
+
<Alert.Content>
|
|
58
|
+
<Stack direction="column" gap="sm">
|
|
59
|
+
{error && (
|
|
60
|
+
<Text size="sm" font="mono" style={{ color: '#dc2626' }}>
|
|
61
|
+
{error.message}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{errorInfo?.componentStack && (
|
|
66
|
+
<Collapsible>
|
|
67
|
+
<Collapsible.Trigger>
|
|
68
|
+
<Text size="xs" color="tertiary" style={{ cursor: 'pointer' }}>
|
|
69
|
+
Show stack trace
|
|
70
|
+
</Text>
|
|
71
|
+
</Collapsible.Trigger>
|
|
72
|
+
<Collapsible.Content>
|
|
73
|
+
<div style={{ marginTop: '8px' }}>
|
|
74
|
+
<CodeBlock language="plaintext">
|
|
75
|
+
{error?.stack || errorInfo.componentStack}
|
|
76
|
+
</CodeBlock>
|
|
77
|
+
</div>
|
|
78
|
+
</Collapsible.Content>
|
|
79
|
+
</Collapsible>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<div>
|
|
83
|
+
<Button variant="danger" size="sm" onClick={this.handleRetry}>
|
|
84
|
+
<RefreshIcon style={{ width: '14px', height: '14px' }} />
|
|
85
|
+
Retry
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</Stack>
|
|
89
|
+
</Alert.Content>
|
|
90
|
+
</Alert.Body>
|
|
91
|
+
</Alert>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.props.children;
|
|
96
|
+
}
|
|
97
|
+
}
|