@fragments-sdk/cli 0.7.17 → 0.8.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.
Files changed (53) hide show
  1. package/dist/bin.js +227 -53
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-QLTLLQBI.js → chunk-2JIKCJX3.js} +312 -24
  4. package/dist/chunk-2JIKCJX3.js.map +1 -0
  5. package/dist/{chunk-57OW43NL.js → chunk-CJEGT3WD.js} +2 -2
  6. package/dist/{chunk-7CRC46HV.js → chunk-GOVI6COW.js} +13 -3
  7. package/dist/chunk-GOVI6COW.js.map +1 -0
  8. package/dist/{chunk-WLXFE6XW.js → chunk-NGIMCIK2.js} +60 -2
  9. package/dist/chunk-NGIMCIK2.js.map +1 -0
  10. package/dist/{chunk-M42XIHPV.js → chunk-WI6SLMSO.js} +2 -2
  11. package/dist/core/index.d.ts +110 -3
  12. package/dist/core/index.js +12 -2
  13. package/dist/{defineFragment-BI9KoPrs.d.ts → defineFragment-D0UTve-I.d.ts} +9 -0
  14. package/dist/{generate-ICIPKCKV.js → generate-35OIMW4Y.js} +4 -4
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +4 -4
  17. package/dist/{init-DIZ6UNBL.js → init-KFYN37ZY.js} +4 -4
  18. package/dist/mcp-bin.js +67 -3
  19. package/dist/mcp-bin.js.map +1 -1
  20. package/dist/{scan-X3DI2X5G.js → scan-65RH3QMM.js} +5 -5
  21. package/dist/{service-JEWWTSKI.js → service-A5GIGGGK.js} +3 -3
  22. package/dist/{static-viewer-JIWCYKVK.js → static-viewer-NSODM5VX.js} +3 -3
  23. package/dist/{test-36UELXTE.js → test-RPWZAYSJ.js} +3 -3
  24. package/dist/{tokens-K2AGUUOJ.js → tokens-NIXSZRX7.js} +4 -4
  25. package/dist/{viewer-QKIAPTPG.js → viewer-HZK4BSDK.js} +43 -12
  26. package/dist/viewer-HZK4BSDK.js.map +1 -0
  27. package/package.json +3 -3
  28. package/src/bin.ts +32 -0
  29. package/src/build.ts +47 -0
  30. package/src/commands/perf.ts +249 -0
  31. package/src/core/bundle-measurer.ts +421 -0
  32. package/src/core/index.ts +16 -0
  33. package/src/core/performance-presets.ts +142 -0
  34. package/src/core/schema.ts +10 -0
  35. package/src/core/types.ts +6 -0
  36. package/src/mcp/server.ts +77 -0
  37. package/src/viewer/components/BottomPanel.tsx +8 -0
  38. package/src/viewer/components/PerformancePanel.tsx +301 -0
  39. package/src/viewer/hooks/useAppState.ts +1 -1
  40. package/src/viewer/vite-plugin.ts +36 -0
  41. package/dist/chunk-7CRC46HV.js.map +0 -1
  42. package/dist/chunk-QLTLLQBI.js.map +0 -1
  43. package/dist/chunk-WLXFE6XW.js.map +0 -1
  44. package/dist/viewer-QKIAPTPG.js.map +0 -1
  45. /package/dist/{chunk-57OW43NL.js.map → chunk-CJEGT3WD.js.map} +0 -0
  46. /package/dist/{chunk-M42XIHPV.js.map → chunk-WI6SLMSO.js.map} +0 -0
  47. /package/dist/{generate-ICIPKCKV.js.map → generate-35OIMW4Y.js.map} +0 -0
  48. /package/dist/{init-DIZ6UNBL.js.map → init-KFYN37ZY.js.map} +0 -0
  49. /package/dist/{scan-X3DI2X5G.js.map → scan-65RH3QMM.js.map} +0 -0
  50. /package/dist/{service-JEWWTSKI.js.map → service-A5GIGGGK.js.map} +0 -0
  51. /package/dist/{static-viewer-JIWCYKVK.js.map → static-viewer-NSODM5VX.js.map} +0 -0
  52. /package/dist/{test-36UELXTE.js.map → test-RPWZAYSJ.js.map} +0 -0
  53. /package/dist/{tokens-K2AGUUOJ.js.map → tokens-NIXSZRX7.js.map} +0 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Performance budget presets and classification utilities.
3
+ *
4
+ * ESLint model: global defaults, zero-config, auto-measurement.
5
+ * Users configure 0-3 numbers. Per-component overrides are the `eslint-disable` equivalent.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface PerformanceBudgets {
13
+ /** Maximum gzipped bundle size in bytes */
14
+ bundleSize: number;
15
+ }
16
+
17
+ export interface PerformanceConfig {
18
+ preset: string;
19
+ budgets: PerformanceBudgets;
20
+ }
21
+
22
+ export type ComplexityTier = 'lightweight' | 'moderate' | 'heavy';
23
+
24
+ export interface PerformanceData {
25
+ /** Gzipped bundle size in bytes */
26
+ bundleSize: number;
27
+ /** Raw (minified, not gzipped) bundle size in bytes */
28
+ rawSize: number;
29
+ /** Complexity classification */
30
+ complexity: ComplexityTier;
31
+ /** Percentage of budget used (0-100+) */
32
+ budgetPercent: number;
33
+ /** Whether the component exceeds its budget */
34
+ overBudget: boolean;
35
+ /** ISO timestamp when measured */
36
+ measuredAt: string;
37
+ }
38
+
39
+ export interface PerformanceSummary {
40
+ /** Preset name used */
41
+ preset: string;
42
+ /** Budget applied in bytes */
43
+ budget: number;
44
+ /** Total components measured */
45
+ total: number;
46
+ /** Number of components over budget */
47
+ overBudget: number;
48
+ /** Distribution by tier */
49
+ tiers: Record<ComplexityTier, number>;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Presets
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const PRESETS: Record<string, PerformanceBudgets> = {
57
+ strict: { bundleSize: 8 * 1024 }, // 8KB gzipped
58
+ standard: { bundleSize: 15 * 1024 }, // 15KB gzipped
59
+ relaxed: { bundleSize: 30 * 1024 }, // 30KB gzipped
60
+ };
61
+
62
+ export const PRESET_NAMES = Object.keys(PRESETS) as readonly string[];
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Resolution
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Resolve a performance config from user input.
70
+ * Accepts a preset name string or a custom config object.
71
+ */
72
+ export function resolvePerformanceConfig(
73
+ input: string | { preset?: string; budgets?: Partial<PerformanceBudgets> } | undefined
74
+ ): PerformanceConfig {
75
+ if (!input) {
76
+ return { preset: 'standard', budgets: PRESETS.standard };
77
+ }
78
+
79
+ if (typeof input === 'string') {
80
+ const budgets = PRESETS[input];
81
+ if (!budgets) {
82
+ throw new Error(
83
+ `Unknown performance preset "${input}". Available: ${PRESET_NAMES.join(', ')}`
84
+ );
85
+ }
86
+ return { preset: input, budgets };
87
+ }
88
+
89
+ const presetName = input.preset ?? 'standard';
90
+ const baseBudgets = PRESETS[presetName];
91
+ if (!baseBudgets) {
92
+ throw new Error(
93
+ `Unknown performance preset "${presetName}". Available: ${PRESET_NAMES.join(', ')}`
94
+ );
95
+ }
96
+
97
+ return {
98
+ preset: presetName,
99
+ budgets: {
100
+ bundleSize: input.budgets?.bundleSize ?? baseBudgets.bundleSize,
101
+ },
102
+ };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Classification
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Classify a component's complexity based on gzipped bundle size.
111
+ *
112
+ * - lightweight: < 5KB — simple, leaf components
113
+ * - moderate: < 15KB — typical composed components
114
+ * - heavy: >= 15KB — complex widgets with dependencies
115
+ */
116
+ export function classifyComplexity(gzipBytes: number): ComplexityTier {
117
+ if (gzipBytes < 5 * 1024) return 'lightweight';
118
+ if (gzipBytes < 15 * 1024) return 'moderate';
119
+ return 'heavy';
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Formatting helpers
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /**
127
+ * Format bytes to a human-readable string (e.g. "2.1KB", "15.3KB").
128
+ */
129
+ export function formatBytes(bytes: number): string {
130
+ if (bytes < 1024) return `${bytes}B`;
131
+ const kb = bytes / 1024;
132
+ return kb < 10 ? `${kb.toFixed(1)}KB` : `${Math.round(kb)}KB`;
133
+ }
134
+
135
+ /**
136
+ * Create a visual budget bar for terminal output.
137
+ */
138
+ export function budgetBar(percent: number, width = 20): string {
139
+ const filled = Math.min(Math.round((percent / 100) * width), width);
140
+ const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
141
+ return percent > 100 ? `\x1b[31m${bar}\x1b[0m` : `\x1b[32m${bar}\x1b[0m`;
142
+ }
@@ -133,6 +133,7 @@ export const fragmentContractSchema = z.object({
133
133
  a11yRules: z.array(z.string()).optional(),
134
134
  bans: z.array(fragmentBanSchema).optional(),
135
135
  scenarioTags: z.array(z.string()).optional(),
136
+ performanceBudget: z.number().positive().optional(),
136
137
  });
137
138
 
138
139
  /**
@@ -203,6 +204,15 @@ export const fragmentsConfigSchema = z.object({
203
204
  requireFullSnippet: z.boolean().optional(),
204
205
  allowedExternalModules: z.array(z.string().min(1)).optional(),
205
206
  }).optional(),
207
+ performance: z.union([
208
+ z.enum(['strict', 'standard', 'relaxed']),
209
+ z.object({
210
+ preset: z.enum(['strict', 'standard', 'relaxed']).optional(),
211
+ budgets: z.object({
212
+ bundleSize: z.number().positive().optional(),
213
+ }).optional(),
214
+ }),
215
+ ]).optional(),
206
216
  });
207
217
 
208
218
  /**
package/src/core/types.ts CHANGED
@@ -303,6 +303,9 @@ export interface FragmentContract {
303
303
 
304
304
  /** Scenario tags for use-case matching (e.g., "form.submit", "navigation.primary") */
305
305
  scenarioTags?: string[];
306
+
307
+ /** Per-component performance budget override in bytes (gzipped). Overrides global budget. */
308
+ performanceBudget?: number;
306
309
  }
307
310
 
308
311
  /**
@@ -493,6 +496,9 @@ export interface FragmentsConfig {
493
496
 
494
497
  /** Snippet/render policy validation */
495
498
  snippets?: SnippetPolicyConfig;
499
+
500
+ /** Performance budgets: preset name or custom config */
501
+ performance?: string | { preset?: string; budgets?: { bundleSize?: number } };
496
502
  }
497
503
 
498
504
  /**
package/src/mcp/server.ts CHANGED
@@ -1536,6 +1536,83 @@ export function createMcpServer(config: McpServerConfig): Server {
1536
1536
  }
1537
1537
  }
1538
1538
 
1539
+ // ================================================================
1540
+ // PERF — query performance data
1541
+ // ================================================================
1542
+ case TOOL_NAMES.perf: {
1543
+ const data = await loadFragments();
1544
+ const componentName = (args?.component as string) ?? undefined;
1545
+ const sort = (args?.sort as string) ?? 'size';
1546
+ const filter = (args?.filter as string) ?? undefined;
1547
+
1548
+ // Collect components with performance data
1549
+ let entries = Object.entries(data.fragments)
1550
+ .filter(([, f]) => f.performance)
1551
+ .map(([name, f]) => ({
1552
+ name,
1553
+ ...f.performance!,
1554
+ }));
1555
+
1556
+ if (entries.length === 0) {
1557
+ return {
1558
+ content: [{
1559
+ type: 'text' as const,
1560
+ text: JSON.stringify({
1561
+ total: 0,
1562
+ components: [],
1563
+ hint: `No performance data found. Run \`${BRAND.cliCommand} perf\` first to measure bundle sizes.`,
1564
+ }, null, 2),
1565
+ }],
1566
+ };
1567
+ }
1568
+
1569
+ // Filter by specific component
1570
+ if (componentName) {
1571
+ entries = entries.filter(
1572
+ (e) => e.name.toLowerCase() === componentName.toLowerCase()
1573
+ );
1574
+ if (entries.length === 0) {
1575
+ throw new Error(
1576
+ `No performance data for "${componentName}". Run \`${BRAND.cliCommand} perf --component ${componentName}\` first.`
1577
+ );
1578
+ }
1579
+ }
1580
+
1581
+ // Filter by tier or over-budget
1582
+ if (filter) {
1583
+ if (filter === 'over-budget') {
1584
+ entries = entries.filter((e) => e.overBudget);
1585
+ } else {
1586
+ entries = entries.filter((e) => e.complexity === filter);
1587
+ }
1588
+ }
1589
+
1590
+ // Sort
1591
+ switch (sort) {
1592
+ case 'name':
1593
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1594
+ break;
1595
+ case 'budget':
1596
+ entries.sort((a, b) => b.budgetPercent - a.budgetPercent);
1597
+ break;
1598
+ case 'size':
1599
+ default:
1600
+ entries.sort((a, b) => b.bundleSize - a.bundleSize);
1601
+ break;
1602
+ }
1603
+
1604
+ return {
1605
+ content: [{
1606
+ type: 'text' as const,
1607
+ text: JSON.stringify({
1608
+ total: entries.length,
1609
+ summary: data.performanceSummary ?? undefined,
1610
+ components: entries,
1611
+ }, null, 2),
1612
+ }],
1613
+ };
1614
+ }
1615
+
1539
1616
  default:
1540
1617
  throw new Error(`Unknown tool: ${name}`);
1541
1618
  }
@@ -14,6 +14,7 @@ import { InteractionsPanel } from './InteractionsPanel.js';
14
14
  import { ActionsPanel } from './ActionsPanel.js';
15
15
  import { ComponentGraph } from './ComponentGraph.js';
16
16
  import { ContractPanel } from './ContractPanel.js';
17
+ import { PerformancePanel } from './PerformancePanel.js';
17
18
  import type { ActivePanel } from '../hooks/useAppState.js';
18
19
  import type { ActionLog } from '../hooks/useActions.js';
19
20
 
@@ -113,6 +114,7 @@ export const BottomPanel = memo(function BottomPanel({
113
114
  </Tabs.Tab>
114
115
  <Tabs.Tab value="graph">Graph</Tabs.Tab>
115
116
  <Tabs.Tab value="contract">Contract</Tabs.Tab>
117
+ <Tabs.Tab value="performance">Performance</Tabs.Tab>
116
118
  </Tabs.List>
117
119
  </Tabs>
118
120
  }
@@ -179,6 +181,12 @@ export const BottomPanel = memo(function BottomPanel({
179
181
  componentName={fragment.meta.name}
180
182
  />
181
183
  )}
184
+
185
+ {activePanel === 'performance' && (
186
+ <PerformancePanel
187
+ componentName={fragment.meta.name}
188
+ />
189
+ )}
182
190
  </ResizablePanel>
183
191
  );
184
192
  });
@@ -0,0 +1,301 @@
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, EmptyState, Alert, Collapsible } from '@fragments-sdk/ui';
15
+ import { BRAND } from '../../core/index.js';
16
+
17
+ interface ImportEntry {
18
+ path: string;
19
+ bytes: number;
20
+ percent: number;
21
+ }
22
+
23
+ interface PerformanceData {
24
+ bundleSize: number;
25
+ rawSize: number;
26
+ complexity: 'lightweight' | 'moderate' | 'heavy';
27
+ budgetPercent: number;
28
+ overBudget: boolean;
29
+ measuredAt: string;
30
+ imports?: ImportEntry[];
31
+ }
32
+
33
+ interface PerfDataResponse {
34
+ summary: {
35
+ preset: string;
36
+ budget: number;
37
+ total: number;
38
+ overBudget: number;
39
+ tiers: Record<string, number>;
40
+ } | null;
41
+ components: Record<string, PerformanceData>;
42
+ }
43
+
44
+ interface PerformancePanelProps {
45
+ componentName: string;
46
+ }
47
+
48
+ function formatBytes(bytes: number): string {
49
+ if (bytes < 1024) return `${bytes} B`;
50
+ const kb = bytes / 1024;
51
+ return kb < 10 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`;
52
+ }
53
+
54
+ function tierVariant(tier: string): 'success' | 'warning' | 'danger' {
55
+ switch (tier) {
56
+ case 'lightweight': return 'success';
57
+ case 'moderate': return 'warning';
58
+ case 'heavy': return 'danger';
59
+ default: return 'warning';
60
+ }
61
+ }
62
+
63
+ function BudgetBar({ percent }: { percent: number }) {
64
+ const capped = Math.min(percent, 100);
65
+ const isOver = percent > 100;
66
+ const color = isOver ? 'var(--fui-color-danger, #e53e3e)'
67
+ : percent > 80 ? 'var(--fui-color-warning, #dd6b20)'
68
+ : 'var(--fui-color-success, #38a169)';
69
+
70
+ return (
71
+ <div style={{ width: '100%' }}>
72
+ <div style={{
73
+ display: 'flex',
74
+ justifyContent: 'space-between',
75
+ marginBottom: '4px',
76
+ }}>
77
+ <Text size="sm" color="secondary">Budget usage</Text>
78
+ <Text size="sm" weight="semibold" style={{ color }}>
79
+ {percent}%
80
+ </Text>
81
+ </div>
82
+ <div style={{
83
+ width: '100%',
84
+ height: '8px',
85
+ borderRadius: '4px',
86
+ backgroundColor: 'var(--fui-color-surface-2, #e2e8f0)',
87
+ overflow: 'hidden',
88
+ }}>
89
+ <div style={{
90
+ width: `${capped}%`,
91
+ height: '100%',
92
+ borderRadius: '4px',
93
+ backgroundColor: color,
94
+ transition: 'width 0.3s ease',
95
+ }} />
96
+ </div>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ /** Labels are already clean from the backend (e.g., "react-markdown", "Markdown (self)") */
102
+
103
+ function ImportBreakdown({ imports, rawSize }: { imports: ImportEntry[]; rawSize: number }) {
104
+ return (
105
+ <Collapsible>
106
+ <Collapsible.Trigger style={{ cursor: 'pointer' }}>
107
+ <Text size="sm" weight="semibold">
108
+ Import breakdown ({imports.length} file{imports.length !== 1 ? 's' : ''})
109
+ </Text>
110
+ </Collapsible.Trigger>
111
+ <Collapsible.Content>
112
+ <div style={{ marginTop: '8px' }}>
113
+ {imports.map((imp) => {
114
+ const barWidth = Math.max(1, Math.round(imp.percent / 2));
115
+ return (
116
+ <div
117
+ key={imp.path}
118
+ style={{
119
+ display: 'grid',
120
+ gridTemplateColumns: '1fr 70px 45px minmax(20px, 100px)',
121
+ alignItems: 'center',
122
+ gap: '8px',
123
+ padding: '4px 0',
124
+ borderBottom: '1px solid var(--fui-color-border, #2d3748)',
125
+ }}
126
+ >
127
+ <Text
128
+ size="xs"
129
+ color="secondary"
130
+ style={{
131
+ overflow: 'hidden',
132
+ textOverflow: 'ellipsis',
133
+ whiteSpace: 'nowrap',
134
+ fontFamily: 'var(--fui-font-mono, monospace)',
135
+ }}
136
+ title={imp.path}
137
+ >
138
+ {imp.path}
139
+ </Text>
140
+ <Text size="xs" weight="semibold" style={{ textAlign: 'right' }}>
141
+ {formatBytes(imp.bytes)}
142
+ </Text>
143
+ <Text size="xs" color="tertiary" style={{ textAlign: 'right' }}>
144
+ {imp.percent}%
145
+ </Text>
146
+ <div style={{
147
+ height: '6px',
148
+ borderRadius: '3px',
149
+ backgroundColor: 'var(--fui-color-surface-2, #2d3748)',
150
+ overflow: 'hidden',
151
+ }}>
152
+ <div style={{
153
+ width: `${Math.min(barWidth * 2, 100)}%`,
154
+ height: '100%',
155
+ borderRadius: '3px',
156
+ backgroundColor: imp.percent > 50
157
+ ? 'var(--fui-color-danger, #e53e3e)'
158
+ : imp.percent > 20
159
+ ? 'var(--fui-color-warning, #dd6b20)'
160
+ : 'var(--fui-color-accent, #4299e1)',
161
+ }} />
162
+ </div>
163
+ </div>
164
+ );
165
+ })}
166
+ </div>
167
+ </Collapsible.Content>
168
+ </Collapsible>
169
+ );
170
+ }
171
+
172
+ export function PerformancePanel({ componentName }: PerformancePanelProps) {
173
+ const [perfData, setPerfData] = useState<PerformanceData | null>(null);
174
+ const [loading, setLoading] = useState(true);
175
+ const [noData, setNoData] = useState(false);
176
+
177
+ useEffect(() => {
178
+ let cancelled = false;
179
+
180
+ async function fetchPerfData() {
181
+ setLoading(true);
182
+ try {
183
+ const res = await fetch('/fragments/perf-data');
184
+ if (!res.ok) {
185
+ setNoData(true);
186
+ return;
187
+ }
188
+ const data: PerfDataResponse = await res.json();
189
+ if (!cancelled) {
190
+ const componentPerf = data.components[componentName];
191
+ if (componentPerf) {
192
+ setPerfData(componentPerf);
193
+ setNoData(false);
194
+ } else {
195
+ setPerfData(null);
196
+ setNoData(true);
197
+ }
198
+ }
199
+ } catch {
200
+ if (!cancelled) {
201
+ setNoData(true);
202
+ }
203
+ } finally {
204
+ if (!cancelled) {
205
+ setLoading(false);
206
+ }
207
+ }
208
+ }
209
+
210
+ fetchPerfData();
211
+ return () => { cancelled = true; };
212
+ }, [componentName]);
213
+
214
+ if (loading) {
215
+ return (
216
+ <div style={{ padding: '16px' }}>
217
+ <Text size="sm" color="secondary">Loading performance data...</Text>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ if (noData || !perfData) {
223
+ return (
224
+ <div style={{ padding: '16px' }}>
225
+ <EmptyState>
226
+ <EmptyState.Title>No performance data</EmptyState.Title>
227
+ <EmptyState.Description>
228
+ Run <code>{BRAND.cliCommand} perf</code> to measure bundle sizes, then reload.
229
+ </EmptyState.Description>
230
+ </EmptyState>
231
+ </div>
232
+ );
233
+ }
234
+
235
+ const { bundleSize, rawSize, complexity, budgetPercent, overBudget, measuredAt } = perfData;
236
+ const measuredDate = new Date(measuredAt).toLocaleString();
237
+
238
+ return (
239
+ <div style={{ padding: '16px' }}>
240
+ <Stack gap="md">
241
+ {overBudget && (
242
+ <Alert variant="danger">
243
+ <strong>{componentName}</strong> exceeds its performance budget ({budgetPercent}% of allowed size).
244
+ Consider code splitting, lazy loading heavy dependencies, or tree-shaking unused exports.
245
+ </Alert>
246
+ )}
247
+
248
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }}>
249
+ <Card>
250
+ <Card.Body>
251
+ <Stack gap="xs">
252
+ <Text size="sm" color="secondary">Gzipped</Text>
253
+ <Text size="xl" weight="bold">{formatBytes(bundleSize)}</Text>
254
+ </Stack>
255
+ </Card.Body>
256
+ </Card>
257
+
258
+ <Card>
259
+ <Card.Body>
260
+ <Stack gap="xs">
261
+ <Text size="sm" color="secondary">Raw (minified)</Text>
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
+ <Text size="sm" color="secondary">Complexity</Text>
271
+ <div>
272
+ <Badge variant={tierVariant(complexity)} size="lg">
273
+ {complexity}
274
+ </Badge>
275
+ </div>
276
+ </Stack>
277
+ </Card.Body>
278
+ </Card>
279
+ </div>
280
+
281
+ <Card>
282
+ <Card.Body>
283
+ <BudgetBar percent={budgetPercent} />
284
+ </Card.Body>
285
+ </Card>
286
+
287
+ {perfData.imports && perfData.imports.length > 0 && (
288
+ <Card>
289
+ <Card.Body>
290
+ <ImportBreakdown imports={perfData.imports} rawSize={rawSize} />
291
+ </Card.Body>
292
+ </Card>
293
+ )}
294
+
295
+ <Text size="xs" color="tertiary">
296
+ Measured {measuredDate}. CSS excluded (JS-only measurement).
297
+ </Text>
298
+ </Stack>
299
+ </div>
300
+ );
301
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useReducer, useCallback, useMemo } from 'react';
7
7
 
8
- export type ActivePanel = 'code' | 'styles' | 'accessibility' | 'interactions' | 'actions' | 'graph' | 'contract';
8
+ export type ActivePanel = 'code' | 'styles' | 'accessibility' | 'interactions' | 'actions' | 'graph' | 'contract' | 'performance';
9
9
 
10
10
  interface AppUIState {
11
11
  activePanel: ActivePanel;
@@ -1412,6 +1412,42 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1412
1412
  return;
1413
1413
  }
1414
1414
 
1415
+ // Handle /fragments/perf-data — serve performance data from fragments.json
1416
+ if (req.url === "/fragments/perf-data" && req.method === "GET") {
1417
+ try {
1418
+ const { readFile: readFileAsync } = await import("node:fs/promises");
1419
+ const { resolve: resolvePath, join: joinPath } = await import("node:path");
1420
+ const outFilePath = resolvePath(
1421
+ projectRoot,
1422
+ config.outFile ?? BRAND.outFile
1423
+ );
1424
+ const raw = await readFileAsync(outFilePath, "utf-8");
1425
+ const parsed = JSON.parse(raw);
1426
+
1427
+ // Extract per-component performance + summary
1428
+ const components: Record<string, unknown> = {};
1429
+ for (const [name, frag] of Object.entries(
1430
+ parsed.fragments as Record<string, any>
1431
+ )) {
1432
+ if (frag.performance) {
1433
+ components[name] = frag.performance;
1434
+ }
1435
+ }
1436
+
1437
+ res.setHeader("Content-Type", "application/json");
1438
+ res.end(
1439
+ JSON.stringify({
1440
+ summary: parsed.performanceSummary ?? null,
1441
+ components,
1442
+ })
1443
+ );
1444
+ } catch {
1445
+ res.setHeader("Content-Type", "application/json");
1446
+ res.end(JSON.stringify({ summary: null, components: {} }));
1447
+ }
1448
+ return;
1449
+ }
1450
+
1415
1451
  // Handle /fragments/preview/ - isolated iframe for component previews
1416
1452
  if (req.url?.startsWith("/fragments/preview")) {
1417
1453
  // Redirect to trailing slash
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Zod schemas for runtime validation of fragment definitions\n */\n\n// Figma property mapping schemas\nconst figmaStringMappingSchema = z.object({\n __type: z.literal('figma-string'),\n figmaProperty: z.string().min(1),\n});\n\nconst figmaBooleanMappingSchema = z.object({\n __type: z.literal('figma-boolean'),\n figmaProperty: z.string().min(1),\n valueMapping: z.object({ true: z.unknown(), false: z.unknown() }).optional(),\n});\n\nconst figmaEnumMappingSchema = z.object({\n __type: z.literal('figma-enum'),\n figmaProperty: z.string().min(1),\n valueMapping: z.record(z.unknown()),\n});\n\nconst figmaInstanceMappingSchema = z.object({\n __type: z.literal('figma-instance'),\n figmaProperty: z.string().min(1),\n});\n\nconst figmaChildrenMappingSchema = z.object({\n __type: z.literal('figma-children'),\n layers: z.array(z.string().min(1)),\n});\n\nconst figmaTextContentMappingSchema = z.object({\n __type: z.literal('figma-text-content'),\n layer: z.string().min(1),\n});\n\nexport const figmaPropMappingSchema = z.discriminatedUnion('__type', [\n figmaStringMappingSchema,\n figmaBooleanMappingSchema,\n figmaEnumMappingSchema,\n figmaInstanceMappingSchema,\n figmaChildrenMappingSchema,\n figmaTextContentMappingSchema,\n]);\n\nexport const fragmentMetaSchema = z.object({\n name: z.string().min(1),\n description: z.string().min(1),\n category: z.string().min(1),\n tags: z.array(z.string()).optional(),\n status: z.enum(['stable', 'beta', 'deprecated', 'experimental']).optional(),\n since: z.string().optional(),\n dependencies: z.array(z.object({\n name: z.string().min(1),\n version: z.string().min(1),\n reason: z.string().optional(),\n })).optional(),\n figma: z.string().url().optional(),\n figmaProps: z.record(figmaPropMappingSchema).optional(),\n});\n\nexport const fragmentUsageSchema = z.object({\n when: z.array(z.string()).min(1),\n whenNot: z.array(z.string()).min(1),\n guidelines: z.array(z.string()).optional(),\n accessibility: z.array(z.string()).optional(),\n});\n\nexport const propTypeSchema: z.ZodType<string> = z.enum([\n 'string',\n 'number',\n 'boolean',\n 'enum',\n 'function',\n 'node',\n 'element',\n 'object',\n 'array',\n 'union',\n 'custom',\n]);\n\nexport const propDefinitionSchema = z.object({\n type: propTypeSchema,\n values: z.array(z.string()).readonly().optional(),\n default: z.unknown().optional(),\n description: z.string().optional(),\n required: z.boolean().optional(),\n constraints: z.array(z.string()).optional(),\n typeDetails: z.record(z.unknown()).optional(),\n});\n\nexport const relationshipTypeSchema = z.enum([\n 'alternative',\n 'sibling',\n 'parent',\n 'child',\n 'composition',\n 'complementary',\n 'used-by',\n]);\n\nexport const componentRelationSchema = z.object({\n component: z.string().min(1),\n relationship: relationshipTypeSchema,\n note: z.string().min(1),\n});\n\nexport const fragmentVariantSchema = z.object({\n name: z.string().min(1),\n description: z.string().min(1),\n render: z.function().returns(z.unknown()),\n code: z.string().optional(),\n figma: z.string().url().optional(),\n});\n\n/**\n * Schema for banned patterns in codebase\n */\nexport const fragmentBanSchema = z.object({\n pattern: z.string().min(1),\n message: z.string().min(1),\n});\n\n/**\n * Schema for agent-optimized contract metadata\n */\nexport const fragmentContractSchema = z.object({\n propsSummary: z.array(z.string()).optional(),\n a11yRules: z.array(z.string()).optional(),\n bans: z.array(fragmentBanSchema).optional(),\n scenarioTags: z.array(z.string()).optional(),\n});\n\n/**\n * Schema for provenance tracking of generated fragments\n */\nexport const fragmentGeneratedSchema = z.object({\n source: z.enum(['storybook', 'manual', 'ai']),\n sourceFile: z.string().optional(),\n confidence: z.number().min(0).max(1).optional(),\n timestamp: z.string().datetime().optional(),\n});\n\n/**\n * Schema for AI-specific metadata for playground context generation\n */\nexport const aiMetadataSchema = z.object({\n compositionPattern: z.enum(['compound', 'simple', 'controlled', 'wrapper']).optional(),\n subComponents: z.array(z.string()).optional(),\n requiredChildren: z.array(z.string()).optional(),\n commonPatterns: z.array(z.string()).optional(),\n});\n\n/**\n * Schema for block definitions\n */\nexport const blockDefinitionSchema = z.object({\n name: z.string().min(1),\n description: z.string().min(1),\n category: z.string().min(1),\n components: z.array(z.string().min(1)).min(1),\n code: z.string().min(1),\n tags: z.array(z.string()).optional(),\n});\n\nexport const fragmentDefinitionSchema = z.object({\n component: z.any(), // Allow any component type (function, class, forwardRef, etc.)\n meta: fragmentMetaSchema,\n usage: fragmentUsageSchema,\n props: z.record(propDefinitionSchema),\n relations: z.array(componentRelationSchema).optional(),\n variants: z.array(fragmentVariantSchema), // Allow empty variants array\n contract: fragmentContractSchema.optional(),\n ai: aiMetadataSchema.optional(),\n _generated: fragmentGeneratedSchema.optional(),\n});\n\n/**\n * Config schema - validates required fields, passes through optional config objects.\n * Type definitions are in types.ts - schema just ensures basic structure.\n */\nexport const fragmentsConfigSchema = z.object({\n include: z.array(z.string()).min(1),\n exclude: z.array(z.string()).optional(),\n components: z.array(z.string()).optional(),\n outFile: z.string().optional(),\n framework: z.enum(['react', 'vue', 'svelte']).optional(),\n figmaFile: z.string().url().optional(),\n figmaToken: z.string().optional(),\n screenshots: z.object({}).passthrough().optional(),\n service: z.object({}).passthrough().optional(),\n registry: z.object({}).passthrough().optional(),\n tokens: z.object({\n include: z.array(z.string()).min(1),\n }).passthrough().optional(),\n snippets: z.object({\n mode: z.enum(['warn', 'error']).optional(),\n scope: z.enum(['snippet', 'snippet+render']).optional(),\n requireFullSnippet: z.boolean().optional(),\n allowedExternalModules: z.array(z.string().min(1)).optional(),\n }).optional(),\n});\n\n/**\n * @deprecated Use blockDefinitionSchema instead\n */\nexport const recipeDefinitionSchema = blockDefinitionSchema;\n"],"mappings":"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","names":[]}