@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,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanelShell — Consistent wrapper for bottom panel tabs.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized structure for all 5 bottom panel tabs:
|
|
5
|
+
* - Optional toolbar (badges, filters, action buttons)
|
|
6
|
+
* - Scrollable body with consistent padding
|
|
7
|
+
* - Loading skeleton state
|
|
8
|
+
* - Empty state with icon, title, description, action
|
|
9
|
+
* - Error state with Alert
|
|
10
|
+
*
|
|
11
|
+
* The tab label (e.g., "Graph", "Performance") is rendered by
|
|
12
|
+
* ResizablePanel — PanelShell does NOT render a title.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { Stack, Box, EmptyState, Alert } from "@fragments-sdk/ui";
|
|
17
|
+
|
|
18
|
+
interface PanelShellEmptyConfig {
|
|
19
|
+
/** Phosphor icon element */
|
|
20
|
+
icon: ReactNode;
|
|
21
|
+
/** Empty state title */
|
|
22
|
+
title: string;
|
|
23
|
+
/** Optional description */
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
/** Optional action slot (e.g. Button, CodeBlock) */
|
|
26
|
+
action?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PanelShellProps {
|
|
30
|
+
/** Optional toolbar content (badges, filters, action buttons) */
|
|
31
|
+
toolbar?: ReactNode;
|
|
32
|
+
/** Main body content */
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
/** Show loading skeleton instead of children */
|
|
35
|
+
loading?: boolean;
|
|
36
|
+
/** Custom loading content (defaults to generic skeleton) */
|
|
37
|
+
loadingContent?: ReactNode;
|
|
38
|
+
/** Empty state config — renders when provided (instead of children) */
|
|
39
|
+
empty?: PanelShellEmptyConfig;
|
|
40
|
+
/** Error message — renders Alert */
|
|
41
|
+
error?: string;
|
|
42
|
+
/** Body padding (default: "md") */
|
|
43
|
+
bodyPadding?: "sm" | "md" | "lg" | "none";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function DefaultSkeleton() {
|
|
47
|
+
return (
|
|
48
|
+
<Stack gap="md">
|
|
49
|
+
{[0, 1, 2].map((i) => (
|
|
50
|
+
<div
|
|
51
|
+
key={i}
|
|
52
|
+
style={{
|
|
53
|
+
height: "48px",
|
|
54
|
+
borderRadius: "8px",
|
|
55
|
+
background: "var(--bg-hover)",
|
|
56
|
+
animation: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
|
57
|
+
animationDelay: `${i * 100}ms`,
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</Stack>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function PanelShell({
|
|
66
|
+
toolbar,
|
|
67
|
+
children,
|
|
68
|
+
loading = false,
|
|
69
|
+
loadingContent,
|
|
70
|
+
empty,
|
|
71
|
+
error,
|
|
72
|
+
bodyPadding = "md",
|
|
73
|
+
}: PanelShellProps) {
|
|
74
|
+
const padding = bodyPadding !== "none" ? bodyPadding : undefined;
|
|
75
|
+
|
|
76
|
+
const renderBody = (): ReactNode => {
|
|
77
|
+
if (loading) {
|
|
78
|
+
return (
|
|
79
|
+
<Box overflow="auto" padding={padding} style={{ flex: 1 }}>
|
|
80
|
+
{loadingContent || <DefaultSkeleton />}
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
return (
|
|
87
|
+
<Box overflow="auto" padding={padding} style={{ flex: 1 }}>
|
|
88
|
+
<Alert variant="danger">{error}</Alert>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (empty) {
|
|
94
|
+
return (
|
|
95
|
+
<Stack
|
|
96
|
+
align="center"
|
|
97
|
+
justify="center"
|
|
98
|
+
style={{ flex: 1, padding: "32px" }}
|
|
99
|
+
>
|
|
100
|
+
<EmptyState>
|
|
101
|
+
<EmptyState.Icon>
|
|
102
|
+
<Box
|
|
103
|
+
rounded="full"
|
|
104
|
+
display="flex"
|
|
105
|
+
style={{
|
|
106
|
+
width: 48,
|
|
107
|
+
height: 48,
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
justifyContent: "center",
|
|
110
|
+
background: "var(--bg-hover)",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{empty.icon}
|
|
114
|
+
</Box>
|
|
115
|
+
</EmptyState.Icon>
|
|
116
|
+
<EmptyState.Title>{empty.title}</EmptyState.Title>
|
|
117
|
+
{empty.description && (
|
|
118
|
+
<EmptyState.Description>
|
|
119
|
+
{empty.description}
|
|
120
|
+
</EmptyState.Description>
|
|
121
|
+
)}
|
|
122
|
+
{empty.action && (
|
|
123
|
+
<Box
|
|
124
|
+
style={{ marginTop: "16px", width: "100%", maxWidth: "400px" }}
|
|
125
|
+
>
|
|
126
|
+
{empty.action}
|
|
127
|
+
</Box>
|
|
128
|
+
)}
|
|
129
|
+
</EmptyState>
|
|
130
|
+
</Stack>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Box overflow="auto" padding={padding} style={{ flex: 1 }}>
|
|
136
|
+
{children}
|
|
137
|
+
</Box>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Stack style={{ height: "100%" }}>
|
|
143
|
+
{toolbar && (
|
|
144
|
+
<Box
|
|
145
|
+
paddingX="sm"
|
|
146
|
+
paddingY="xs"
|
|
147
|
+
borderBottom
|
|
148
|
+
style={{
|
|
149
|
+
flexShrink: 0,
|
|
150
|
+
minHeight: "36px",
|
|
151
|
+
display: "flex",
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{toolbar}
|
|
156
|
+
</Box>
|
|
157
|
+
)}
|
|
158
|
+
{renderBody()}
|
|
159
|
+
</Stack>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Panel — bundle size visualization in the viewer.
|
|
3
|
+
*
|
|
4
|
+
* Fetches performance data from /fragments/perf-data (served by the
|
|
5
|
+
* Vite dev server from fragments.json) and displays:
|
|
6
|
+
* - Gzipped and raw bundle size
|
|
7
|
+
* - Complexity tier badge
|
|
8
|
+
* - Budget bar with percentage
|
|
9
|
+
* - Over-budget alert when applicable
|
|
10
|
+
* - Empty state when no data (prompts `fragments perf`)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState, useEffect } from 'react';
|
|
14
|
+
import { Card, Badge, Text, Stack, Alert, Collapsible, Box } from '@fragments-sdk/ui';
|
|
15
|
+
import { Lightning, Package, FileCode, Star } from '@phosphor-icons/react';
|
|
16
|
+
import { BRAND } from '@fragments-sdk/core';
|
|
17
|
+
import { PanelShell } from './PanelShell.js';
|
|
18
|
+
|
|
19
|
+
interface ImportEntry {
|
|
20
|
+
path: string;
|
|
21
|
+
bytes: number;
|
|
22
|
+
percent: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PerformanceData {
|
|
26
|
+
bundleSize: number;
|
|
27
|
+
rawSize: number;
|
|
28
|
+
complexity: 'lightweight' | 'moderate' | 'heavy';
|
|
29
|
+
budgetPercent: number;
|
|
30
|
+
overBudget: boolean;
|
|
31
|
+
measuredAt: string;
|
|
32
|
+
imports?: ImportEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PerfDataResponse {
|
|
36
|
+
summary: {
|
|
37
|
+
preset: string;
|
|
38
|
+
budget: number;
|
|
39
|
+
total: number;
|
|
40
|
+
overBudget: number;
|
|
41
|
+
tiers: Record<string, number>;
|
|
42
|
+
} | null;
|
|
43
|
+
components: Record<string, PerformanceData>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PerformancePanelProps {
|
|
47
|
+
componentName: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatBytes(bytes: number): string {
|
|
51
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
52
|
+
const kb = bytes / 1024;
|
|
53
|
+
return kb < 10 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tierVariant(tier: string): 'success' | 'warning' | 'danger' {
|
|
57
|
+
switch (tier) {
|
|
58
|
+
case 'lightweight': return 'success';
|
|
59
|
+
case 'moderate': return 'warning';
|
|
60
|
+
case 'heavy': return 'danger';
|
|
61
|
+
default: return 'warning';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function BudgetBar({ percent }: { percent: number }) {
|
|
66
|
+
const capped = Math.min(percent, 100);
|
|
67
|
+
const isOver = percent > 100;
|
|
68
|
+
const color = isOver ? 'var(--fui-color-danger, #e53e3e)'
|
|
69
|
+
: percent > 80 ? 'var(--fui-color-warning, #dd6b20)'
|
|
70
|
+
: 'var(--fui-color-success, #38a169)';
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div style={{ width: '100%' }}>
|
|
74
|
+
<div style={{
|
|
75
|
+
display: 'flex',
|
|
76
|
+
justifyContent: 'space-between',
|
|
77
|
+
marginBottom: '4px',
|
|
78
|
+
}}>
|
|
79
|
+
<Text size="sm" color="secondary">Budget usage</Text>
|
|
80
|
+
<Text size="sm" weight="semibold" style={{ color }}>
|
|
81
|
+
{percent}%
|
|
82
|
+
</Text>
|
|
83
|
+
</div>
|
|
84
|
+
<div style={{
|
|
85
|
+
width: '100%',
|
|
86
|
+
height: '8px',
|
|
87
|
+
borderRadius: '4px',
|
|
88
|
+
backgroundColor: 'var(--fui-color-surface-2, #e2e8f0)',
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
}}>
|
|
91
|
+
<div style={{
|
|
92
|
+
width: `${capped}%`,
|
|
93
|
+
height: '100%',
|
|
94
|
+
borderRadius: '4px',
|
|
95
|
+
backgroundColor: color,
|
|
96
|
+
transition: 'width 0.3s ease',
|
|
97
|
+
}} />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Labels are already clean from the backend (e.g., "react-markdown", "Markdown (self)") */
|
|
104
|
+
|
|
105
|
+
function ImportBreakdown({ imports, rawSize }: { imports: ImportEntry[]; rawSize: number }) {
|
|
106
|
+
return (
|
|
107
|
+
<Collapsible>
|
|
108
|
+
<Collapsible.Trigger style={{ cursor: 'pointer' }}>
|
|
109
|
+
<Text size="sm" weight="semibold">
|
|
110
|
+
Import breakdown ({imports.length} file{imports.length !== 1 ? 's' : ''})
|
|
111
|
+
</Text>
|
|
112
|
+
</Collapsible.Trigger>
|
|
113
|
+
<Collapsible.Content>
|
|
114
|
+
<div style={{ marginTop: '8px' }}>
|
|
115
|
+
{imports.map((imp) => {
|
|
116
|
+
const barWidth = Math.max(1, Math.round(imp.percent / 2));
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
key={imp.path}
|
|
120
|
+
style={{
|
|
121
|
+
display: 'grid',
|
|
122
|
+
gridTemplateColumns: '1fr 70px 45px minmax(20px, 100px)',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
gap: '8px',
|
|
125
|
+
padding: '4px 0',
|
|
126
|
+
borderBottom: '1px solid var(--fui-color-border, #2d3748)',
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<Text
|
|
130
|
+
size="xs"
|
|
131
|
+
color="secondary"
|
|
132
|
+
style={{
|
|
133
|
+
overflow: 'hidden',
|
|
134
|
+
textOverflow: 'ellipsis',
|
|
135
|
+
whiteSpace: 'nowrap',
|
|
136
|
+
fontFamily: 'var(--fui-font-mono, monospace)',
|
|
137
|
+
}}
|
|
138
|
+
title={imp.path}
|
|
139
|
+
>
|
|
140
|
+
{imp.path}
|
|
141
|
+
</Text>
|
|
142
|
+
<Text size="xs" weight="semibold" style={{ textAlign: 'right' }}>
|
|
143
|
+
{formatBytes(imp.bytes)}
|
|
144
|
+
</Text>
|
|
145
|
+
<Text size="xs" color="tertiary" style={{ textAlign: 'right' }}>
|
|
146
|
+
{imp.percent}%
|
|
147
|
+
</Text>
|
|
148
|
+
<div style={{
|
|
149
|
+
height: '6px',
|
|
150
|
+
borderRadius: '3px',
|
|
151
|
+
backgroundColor: 'var(--fui-color-surface-2, #2d3748)',
|
|
152
|
+
overflow: 'hidden',
|
|
153
|
+
}}>
|
|
154
|
+
<div style={{
|
|
155
|
+
width: `${Math.min(barWidth * 2, 100)}%`,
|
|
156
|
+
height: '100%',
|
|
157
|
+
borderRadius: '3px',
|
|
158
|
+
backgroundColor: imp.percent > 50
|
|
159
|
+
? 'var(--fui-color-danger, #e53e3e)'
|
|
160
|
+
: imp.percent > 20
|
|
161
|
+
? 'var(--fui-color-warning, #dd6b20)'
|
|
162
|
+
: 'var(--fui-color-accent, #4299e1)',
|
|
163
|
+
}} />
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
</Collapsible.Content>
|
|
170
|
+
</Collapsible>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function PerformancePanel({ componentName }: PerformancePanelProps) {
|
|
175
|
+
const [perfData, setPerfData] = useState<PerformanceData | null>(null);
|
|
176
|
+
const [loading, setLoading] = useState(true);
|
|
177
|
+
const [noData, setNoData] = useState(false);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
let cancelled = false;
|
|
181
|
+
|
|
182
|
+
async function fetchPerfData() {
|
|
183
|
+
setLoading(true);
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch('/fragments/perf-data');
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
setNoData(true);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const data: PerfDataResponse = await res.json();
|
|
191
|
+
if (!cancelled) {
|
|
192
|
+
const componentPerf = data.components[componentName];
|
|
193
|
+
if (componentPerf) {
|
|
194
|
+
setPerfData(componentPerf);
|
|
195
|
+
setNoData(false);
|
|
196
|
+
} else {
|
|
197
|
+
setPerfData(null);
|
|
198
|
+
setNoData(true);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
if (!cancelled) {
|
|
203
|
+
setNoData(true);
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
if (!cancelled) {
|
|
207
|
+
setLoading(false);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fetchPerfData();
|
|
213
|
+
return () => { cancelled = true; };
|
|
214
|
+
}, [componentName]);
|
|
215
|
+
|
|
216
|
+
const showEmpty = !loading && (noData || !perfData);
|
|
217
|
+
|
|
218
|
+
const emptyConfig = showEmpty ? {
|
|
219
|
+
icon: <Lightning size={24} weight="regular" style={{ color: 'var(--text-tertiary)' }} />,
|
|
220
|
+
title: "No performance data",
|
|
221
|
+
description: (
|
|
222
|
+
<>
|
|
223
|
+
Run <Box as="code" padding="xs" background="secondary" rounded="sm" style={{ fontSize: '12px', display: 'inline', fontFamily: 'var(--fui-font-mono, monospace)' }}>{BRAND.cliCommand} perf</Box> to measure bundle sizes, then reload the viewer.
|
|
224
|
+
</>
|
|
225
|
+
),
|
|
226
|
+
} : undefined;
|
|
227
|
+
|
|
228
|
+
// Safe destructure — only used when PanelShell renders children (not loading/empty)
|
|
229
|
+
const { bundleSize = 0, rawSize = 0, complexity = 'lightweight' as const, budgetPercent = 0, overBudget = false, measuredAt = '' } = perfData || {};
|
|
230
|
+
const measuredDate = measuredAt ? new Date(measuredAt).toLocaleString() : '';
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<PanelShell loading={loading} empty={emptyConfig}>
|
|
234
|
+
<Stack gap="md">
|
|
235
|
+
{overBudget && (
|
|
236
|
+
<Alert variant="danger">
|
|
237
|
+
<strong>{componentName}</strong> exceeds its performance budget ({budgetPercent}% of allowed size).
|
|
238
|
+
Consider code splitting, lazy loading heavy dependencies, or tree-shaking unused exports.
|
|
239
|
+
</Alert>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }}>
|
|
243
|
+
<Card>
|
|
244
|
+
<Card.Body>
|
|
245
|
+
<Stack gap="xs">
|
|
246
|
+
<Stack direction="row" align="center" gap="xs">
|
|
247
|
+
<Package size={14} weight="regular" style={{ color: 'var(--text-tertiary)' }} />
|
|
248
|
+
<Text size="xs" color="secondary">Gzipped</Text>
|
|
249
|
+
</Stack>
|
|
250
|
+
<Text size="xl" weight="bold">{formatBytes(bundleSize)}</Text>
|
|
251
|
+
</Stack>
|
|
252
|
+
</Card.Body>
|
|
253
|
+
</Card>
|
|
254
|
+
|
|
255
|
+
<Card>
|
|
256
|
+
<Card.Body>
|
|
257
|
+
<Stack gap="xs">
|
|
258
|
+
<Stack direction="row" align="center" gap="xs">
|
|
259
|
+
<FileCode size={14} weight="regular" style={{ color: 'var(--text-tertiary)' }} />
|
|
260
|
+
<Text size="xs" color="secondary">Raw (minified)</Text>
|
|
261
|
+
</Stack>
|
|
262
|
+
<Text size="xl" weight="bold">{formatBytes(rawSize)}</Text>
|
|
263
|
+
</Stack>
|
|
264
|
+
</Card.Body>
|
|
265
|
+
</Card>
|
|
266
|
+
|
|
267
|
+
<Card>
|
|
268
|
+
<Card.Body>
|
|
269
|
+
<Stack gap="xs">
|
|
270
|
+
<Stack direction="row" align="center" gap="xs">
|
|
271
|
+
<Star size={14} weight="regular" style={{ color: 'var(--text-tertiary)' }} />
|
|
272
|
+
<Text size="xs" color="secondary">Complexity</Text>
|
|
273
|
+
</Stack>
|
|
274
|
+
<div>
|
|
275
|
+
<Badge variant={tierVariant(complexity)} size="lg">
|
|
276
|
+
{complexity}
|
|
277
|
+
</Badge>
|
|
278
|
+
</div>
|
|
279
|
+
</Stack>
|
|
280
|
+
</Card.Body>
|
|
281
|
+
</Card>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<Card>
|
|
285
|
+
<Card.Body>
|
|
286
|
+
<BudgetBar percent={budgetPercent} />
|
|
287
|
+
</Card.Body>
|
|
288
|
+
</Card>
|
|
289
|
+
|
|
290
|
+
{perfData?.imports && perfData.imports.length > 0 && (
|
|
291
|
+
<Card>
|
|
292
|
+
<Card.Body>
|
|
293
|
+
<ImportBreakdown imports={perfData.imports} rawSize={rawSize} />
|
|
294
|
+
</Card.Body>
|
|
295
|
+
</Card>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
<Text size="xs" color="tertiary">
|
|
299
|
+
Measured {measuredDate}. CSS excluded (JS-only measurement).
|
|
300
|
+
</Text>
|
|
301
|
+
</Stack>
|
|
302
|
+
</PanelShell>
|
|
303
|
+
);
|
|
304
|
+
}
|