@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.
Files changed (141) hide show
  1. package/LICENSE +84 -0
  2. package/index.html +28 -0
  3. package/package.json +71 -0
  4. package/src/__tests__/a11y-fixes.test.ts +358 -0
  5. package/src/__tests__/jsx-parser.test.ts +502 -0
  6. package/src/__tests__/render-utils.test.ts +232 -0
  7. package/src/__tests__/style-utils.test.ts +404 -0
  8. package/src/app/index.ts +1 -0
  9. package/src/assets/fragments-logo.ts +4 -0
  10. package/src/assets/fragments_logo.png +0 -0
  11. package/src/components/AccessibilityPanel.tsx +1457 -0
  12. package/src/components/ActionCapture.tsx +172 -0
  13. package/src/components/ActionsPanel.tsx +332 -0
  14. package/src/components/AllVariantsPreview.tsx +78 -0
  15. package/src/components/App.tsx +604 -0
  16. package/src/components/BottomPanel.tsx +288 -0
  17. package/src/components/CodePanel.naming.test.tsx +59 -0
  18. package/src/components/CodePanel.tsx +118 -0
  19. package/src/components/CommandPalette.tsx +392 -0
  20. package/src/components/ComponentDocView.tsx +164 -0
  21. package/src/components/ComponentGraph.tsx +380 -0
  22. package/src/components/ComponentHeader.tsx +88 -0
  23. package/src/components/ContractPanel.tsx +241 -0
  24. package/src/components/DeviceMockup.tsx +156 -0
  25. package/src/components/EmptyVariantMessage.tsx +54 -0
  26. package/src/components/ErrorBoundary.tsx +97 -0
  27. package/src/components/FigmaEmbed.tsx +238 -0
  28. package/src/components/FragmentEditor.tsx +525 -0
  29. package/src/components/FragmentRenderer.tsx +61 -0
  30. package/src/components/HeaderSearch.tsx +24 -0
  31. package/src/components/HealthDashboard.tsx +441 -0
  32. package/src/components/HmrStatusIndicator.tsx +61 -0
  33. package/src/components/Icons.tsx +479 -0
  34. package/src/components/InteractionsPanel.tsx +757 -0
  35. package/src/components/IsolatedPreviewFrame.tsx +390 -0
  36. package/src/components/IsolatedRender.tsx +113 -0
  37. package/src/components/KeyboardShortcutsHelp.tsx +53 -0
  38. package/src/components/LandingPage.tsx +420 -0
  39. package/src/components/Layout.tsx +27 -0
  40. package/src/components/LeftSidebar.tsx +472 -0
  41. package/src/components/LoadErrorMessage.tsx +102 -0
  42. package/src/components/MultiViewportPreview.tsx +527 -0
  43. package/src/components/NoVariantsMessage.tsx +59 -0
  44. package/src/components/PanelShell.tsx +161 -0
  45. package/src/components/PerformancePanel.tsx +304 -0
  46. package/src/components/PreviewArea.tsx +254 -0
  47. package/src/components/PreviewAside.tsx +168 -0
  48. package/src/components/PreviewFrameHost.tsx +304 -0
  49. package/src/components/PreviewToolbar.tsx +80 -0
  50. package/src/components/PropsEditor.tsx +506 -0
  51. package/src/components/PropsTable.tsx +111 -0
  52. package/src/components/RelationsSection.tsx +88 -0
  53. package/src/components/ResizablePanel.tsx +271 -0
  54. package/src/components/RightSidebar.tsx +102 -0
  55. package/src/components/RuntimeToolsRegistrar.tsx +17 -0
  56. package/src/components/ScreenshotButton.tsx +90 -0
  57. package/src/components/ShadowPreview.tsx +204 -0
  58. package/src/components/Sidebar.tsx +169 -0
  59. package/src/components/SkeletonLoader.tsx +161 -0
  60. package/src/components/ThemeProvider.tsx +42 -0
  61. package/src/components/Toast.tsx +3 -0
  62. package/src/components/TokenStylePanel.tsx +699 -0
  63. package/src/components/TopToolbar.tsx +159 -0
  64. package/src/components/Untitled +1 -0
  65. package/src/components/UsageSection.tsx +95 -0
  66. package/src/components/VariantMatrix.tsx +391 -0
  67. package/src/components/VariantRenderer.tsx +131 -0
  68. package/src/components/VariantTabs.tsx +40 -0
  69. package/src/components/ViewerHeader.tsx +69 -0
  70. package/src/components/ViewerStateSync.tsx +52 -0
  71. package/src/components/ViewportSelector.tsx +172 -0
  72. package/src/components/WebMCPDevTools.tsx +503 -0
  73. package/src/components/WebMCPIntegration.tsx +47 -0
  74. package/src/components/WebMCPStatusIndicator.tsx +60 -0
  75. package/src/components/_future/CreatePage.tsx +835 -0
  76. package/src/components/viewer-utils.ts +16 -0
  77. package/src/composition-renderer.ts +381 -0
  78. package/src/constants/index.ts +1 -0
  79. package/src/constants/ui.ts +166 -0
  80. package/src/entry.tsx +335 -0
  81. package/src/hooks/index.ts +2 -0
  82. package/src/hooks/useA11yCache.ts +383 -0
  83. package/src/hooks/useA11yService.ts +364 -0
  84. package/src/hooks/useActions.ts +138 -0
  85. package/src/hooks/useAppState.ts +147 -0
  86. package/src/hooks/useCompiledFragments.ts +42 -0
  87. package/src/hooks/useFigmaIntegration.ts +132 -0
  88. package/src/hooks/useHmrStatus.ts +109 -0
  89. package/src/hooks/useKeyboardShortcuts.ts +270 -0
  90. package/src/hooks/usePreviewBridge.ts +347 -0
  91. package/src/hooks/useScrollSpy.ts +78 -0
  92. package/src/hooks/useShadowStyles.ts +221 -0
  93. package/src/hooks/useUrlState.ts +318 -0
  94. package/src/hooks/useViewSettings.ts +111 -0
  95. package/src/intelligence/healthReport.ts +505 -0
  96. package/src/intelligence/styleDrift.ts +340 -0
  97. package/src/intelligence/usageScanner.ts +309 -0
  98. package/src/jsx-parser.ts +486 -0
  99. package/src/preview-frame-entry.tsx +25 -0
  100. package/src/preview-frame.html +148 -0
  101. package/src/render-template.html +68 -0
  102. package/src/render-utils.ts +311 -0
  103. package/src/shared/ComponentDocContent.module.scss +10 -0
  104. package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
  105. package/src/shared/ComponentDocContent.tsx +274 -0
  106. package/src/shared/DocsHeaderBar.tsx +129 -0
  107. package/src/shared/DocsPageAsideHost.tsx +89 -0
  108. package/src/shared/DocsPageShell.tsx +124 -0
  109. package/src/shared/DocsSearchCommand.tsx +99 -0
  110. package/src/shared/DocsSidebarNav.tsx +66 -0
  111. package/src/shared/PropsTable.module.scss +68 -0
  112. package/src/shared/PropsTable.module.scss.d.ts +2 -0
  113. package/src/shared/PropsTable.tsx +76 -0
  114. package/src/shared/VariantPreviewCard.module.scss +114 -0
  115. package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
  116. package/src/shared/VariantPreviewCard.tsx +137 -0
  117. package/src/shared/docs-data/index.ts +32 -0
  118. package/src/shared/docs-data/mcp-configs.ts +72 -0
  119. package/src/shared/docs-data/palettes.ts +75 -0
  120. package/src/shared/docs-data/setup-examples.ts +55 -0
  121. package/src/shared/docs-layout.scss +28 -0
  122. package/src/shared/docs-layout.scss.d.ts +2 -0
  123. package/src/shared/index.ts +34 -0
  124. package/src/shared/types.ts +53 -0
  125. package/src/style-utils.ts +414 -0
  126. package/src/styles/globals.css +278 -0
  127. package/src/types/a11y.ts +197 -0
  128. package/src/utils/a11y-fixes.ts +509 -0
  129. package/src/utils/actionExport.ts +372 -0
  130. package/src/utils/colorSchemes.ts +201 -0
  131. package/src/utils/contrast.ts +246 -0
  132. package/src/utils/detectRelationships.ts +256 -0
  133. package/src/webmcp/__tests__/analytics.test.ts +108 -0
  134. package/src/webmcp/analytics.ts +165 -0
  135. package/src/webmcp/index.ts +3 -0
  136. package/src/webmcp/posthog-bridge.ts +39 -0
  137. package/src/webmcp/runtime-tools.ts +152 -0
  138. package/src/webmcp/scan-utils.ts +135 -0
  139. package/src/webmcp/use-tool-analytics.ts +69 -0
  140. package/src/webmcp/viewer-state.ts +45 -0
  141. package/tsconfig.json +20 -0
@@ -0,0 +1,68 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Fragments Render</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <style>
14
+ /* Reset and base styles for isolated rendering */
15
+ *, *::before, *::after {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ html, body {
20
+ margin: 0;
21
+ padding: 0;
22
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
23
+ -webkit-font-smoothing: antialiased;
24
+ -moz-osx-font-smoothing: grayscale;
25
+ }
26
+
27
+ body {
28
+ background: #ffffff;
29
+ min-height: 100vh;
30
+ }
31
+
32
+ #render-root {
33
+ padding: 16px;
34
+ display: inline-block;
35
+ }
36
+
37
+ /* Signal that rendering is complete */
38
+ #render-root.ready {
39
+ /* Used by Playwright to know when to capture */
40
+ }
41
+
42
+ /* Error display */
43
+ .render-error {
44
+ padding: 16px;
45
+ background: #fef2f2;
46
+ border: 1px solid #fecaca;
47
+ border-radius: 8px;
48
+ color: #dc2626;
49
+ font-size: 14px;
50
+ }
51
+
52
+ .render-error pre {
53
+ margin: 8px 0 0;
54
+ padding: 8px;
55
+ background: #fff;
56
+ border-radius: 4px;
57
+ overflow-x: auto;
58
+ font-size: 12px;
59
+ font-family: 'JetBrains Mono', monospace;
60
+ }
61
+ </style>
62
+ <!-- PROJECT_STYLES_PLACEHOLDER -->
63
+ </head>
64
+ <body>
65
+ <div id="render-root"></div>
66
+ <!-- RENDER_SCRIPT_PLACEHOLDER -->
67
+ </body>
68
+ </html>
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Render utilities for AI preview endpoint.
3
+ * Generates code to render design system components in isolation.
4
+ */
5
+
6
+ export interface RenderRequest {
7
+ /** Component name (e.g., "Button", "Card") */
8
+ component: string;
9
+ /** Props to pass to the component */
10
+ props?: Record<string, unknown>;
11
+ /** Variant name to render (uses variant's render function) */
12
+ variant?: string;
13
+ /** Viewport dimensions */
14
+ viewport?: {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ }
19
+
20
+ export interface FragmentInfo {
21
+ name: string;
22
+ path: string;
23
+ }
24
+
25
+ /**
26
+ * Serialize a value to JavaScript code string.
27
+ * Handles strings, numbers, booleans, null, undefined, arrays, and objects.
28
+ */
29
+ export function serializeValue(value: unknown): string {
30
+ if (value === null) return "null";
31
+ if (value === undefined) return "undefined";
32
+ if (typeof value === "string") return JSON.stringify(value);
33
+ if (typeof value === "number") return String(value);
34
+ if (typeof value === "boolean") return String(value);
35
+ if (Array.isArray(value)) {
36
+ return `[${value.map(serializeValue).join(", ")}]`;
37
+ }
38
+ if (typeof value === "object") {
39
+ const entries = Object.entries(value)
40
+ .map(([k, v]) => `${JSON.stringify(k)}: ${serializeValue(v)}`)
41
+ .join(", ");
42
+ return `{${entries}}`;
43
+ }
44
+ // Functions and other types - skip
45
+ return "undefined";
46
+ }
47
+
48
+ /**
49
+ * Serialize props object to JSX attribute string.
50
+ * Example: { variant: "primary", disabled: true } -> variant="primary" disabled={true}
51
+ */
52
+ export function serializePropsToJsx(props: Record<string, unknown>): string {
53
+ return Object.entries(props)
54
+ .filter(([_, v]) => v !== undefined)
55
+ .map(([key, value]) => {
56
+ if (typeof value === "string") {
57
+ return `${key}=${JSON.stringify(value)}`;
58
+ }
59
+ return `${key}={${serializeValue(value)}}`;
60
+ })
61
+ .join(" ");
62
+ }
63
+
64
+ /**
65
+ * Find a fragment by component name.
66
+ * Returns the fragment info if found, null otherwise.
67
+ */
68
+ export function findFragmentByName(
69
+ componentName: string,
70
+ fragments: Array<{ path: string; fragment: { meta: { name: string } } }>
71
+ ): FragmentInfo | null {
72
+ const match = fragments.find(
73
+ (s) => s.fragment.meta.name.toLowerCase() === componentName.toLowerCase()
74
+ );
75
+
76
+ if (!match) return null;
77
+
78
+ return {
79
+ name: match.fragment.meta.name,
80
+ path: match.path,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Get list of available component names from loaded fragments.
86
+ */
87
+ export function getAvailableComponents(
88
+ fragments: Array<{ fragment: { meta: { name: string } } }>
89
+ ): string[] {
90
+ return fragments.map((s) => s.fragment.meta.name).sort();
91
+ }
92
+
93
+ /**
94
+ * Generate the render script that will be injected into the template.
95
+ * This script imports the component and renders it with the given props.
96
+ */
97
+ export function generateRenderScript(
98
+ fragmentPath: string,
99
+ componentName: string,
100
+ props: Record<string, unknown> = {}
101
+ ): string {
102
+ const propsJsx = serializePropsToJsx(props);
103
+ const propsString = propsJsx ? ` ${propsJsx}` : "";
104
+
105
+ // Handle children prop specially - render as content between tags
106
+ const hasChildren = "children" in props && props.children !== undefined;
107
+ const childrenContent = hasChildren ? String(props.children) : "";
108
+ const propsWithoutChildren = { ...props };
109
+ delete propsWithoutChildren.children;
110
+ const propsJsxNoChildren = serializePropsToJsx(propsWithoutChildren);
111
+ const propsStringNoChildren = propsJsxNoChildren ? ` ${propsJsxNoChildren}` : "";
112
+
113
+ return `
114
+ import React from "react";
115
+ import { createRoot } from "react-dom/client";
116
+
117
+ // Import the fragment to get the component
118
+ async function render() {
119
+ const root = document.getElementById("render-root");
120
+
121
+ try {
122
+ // Dynamic import of the fragment file
123
+ const fragmentModule = await import("${fragmentPath}");
124
+ const fragment = fragmentModule.default;
125
+
126
+ if (!fragment || !fragment.component) {
127
+ throw new Error("Fragment does not export a component");
128
+ }
129
+
130
+ const Component = fragment.component;
131
+
132
+ // Create React root and render
133
+ const reactRoot = createRoot(root);
134
+ ${
135
+ hasChildren
136
+ ? `reactRoot.render(React.createElement(Component, ${JSON.stringify(propsWithoutChildren)}, ${JSON.stringify(childrenContent)}));`
137
+ : `reactRoot.render(React.createElement(Component, ${JSON.stringify(props)}));`
138
+ }
139
+
140
+ // Signal that rendering is complete
141
+ // Wait a frame for React to flush
142
+ requestAnimationFrame(() => {
143
+ requestAnimationFrame(() => {
144
+ root.classList.add("ready");
145
+ window.__RENDER_READY__ = true;
146
+ });
147
+ });
148
+ } catch (error) {
149
+ console.error("Render error:", error);
150
+ root.innerHTML = \`
151
+ <div class="render-error">
152
+ <strong>Render Error</strong>
153
+ <pre>\${error.message}</pre>
154
+ </div>
155
+ \`;
156
+ root.classList.add("ready");
157
+ window.__RENDER_READY__ = true;
158
+ window.__RENDER_ERROR__ = error.message;
159
+ }
160
+ }
161
+
162
+ render();
163
+ `;
164
+ }
165
+
166
+ /**
167
+ * Generate a render script that renders a specific variant by name.
168
+ * The variant lookup happens in the browser using the fragment's variants array.
169
+ */
170
+ export function generateVariantRenderScript(
171
+ fragmentPath: string,
172
+ componentName: string,
173
+ variantName: string
174
+ ): string {
175
+ const variantNameLower = JSON.stringify(variantName.toLowerCase());
176
+
177
+ return `
178
+ import React from "react";
179
+ import { createRoot } from "react-dom/client";
180
+
181
+ async function render() {
182
+ const root = document.getElementById("render-root");
183
+
184
+ try {
185
+ const fragmentModule = await import("${fragmentPath}");
186
+ const fragment = fragmentModule.default;
187
+
188
+ if (!fragment || !fragment.variants || fragment.variants.length === 0) {
189
+ throw new Error("Fragment has no variants");
190
+ }
191
+
192
+ const variant = fragment.variants.find(
193
+ v => v.name.toLowerCase() === ${variantNameLower}
194
+ );
195
+
196
+ if (!variant) {
197
+ const available = fragment.variants.map(v => v.name).join(", ");
198
+ throw new Error("Variant '" + ${JSON.stringify(variantName)} + "' not found. Available: " + available);
199
+ }
200
+
201
+ const element = variant.render();
202
+
203
+ const reactRoot = createRoot(root);
204
+ reactRoot.render(element);
205
+
206
+ requestAnimationFrame(() => {
207
+ requestAnimationFrame(() => {
208
+ root.classList.add("ready");
209
+ window.__RENDER_READY__ = true;
210
+ });
211
+ });
212
+ } catch (error) {
213
+ console.error("Render error:", error);
214
+ root.innerHTML = \`
215
+ <div class="render-error">
216
+ <strong>Render Error</strong>
217
+ <pre>\${error.message}</pre>
218
+ </div>
219
+ \`;
220
+ root.classList.add("ready");
221
+ window.__RENDER_READY__ = true;
222
+ window.__RENDER_ERROR__ = error.message;
223
+ }
224
+ }
225
+
226
+ render();
227
+ `;
228
+ }
229
+
230
+ /**
231
+ * Generate a render script that also runs axe-core for accessibility auditing.
232
+ * When variantName is provided, renders that specific variant; otherwise renders
233
+ * the component with empty props.
234
+ */
235
+ export function generateA11yRenderScript(
236
+ fragmentPath: string,
237
+ componentName: string,
238
+ variantName?: string
239
+ ): string {
240
+ const variantLookup = variantName
241
+ ? `
242
+ const variant = fragment.variants?.find(
243
+ v => v.name.toLowerCase() === ${JSON.stringify(variantName.toLowerCase())}
244
+ );
245
+ if (!variant) {
246
+ throw new Error("Variant '${variantName}' not found");
247
+ }
248
+ element = variant.render();`
249
+ : `
250
+ element = React.createElement(fragment.component, {});`;
251
+
252
+ return `
253
+ import React from "react";
254
+ import { createRoot } from "react-dom/client";
255
+ import axe from "axe-core";
256
+
257
+ async function render() {
258
+ const root = document.getElementById("render-root");
259
+
260
+ try {
261
+ const fragmentModule = await import("${fragmentPath}");
262
+ const fragment = fragmentModule.default;
263
+
264
+ if (!fragment || !fragment.component) {
265
+ throw new Error("Fragment does not export a component");
266
+ }
267
+
268
+ let element;
269
+ ${variantLookup}
270
+
271
+ const reactRoot = createRoot(root);
272
+ reactRoot.render(element);
273
+
274
+ // Wait for React to flush rendering
275
+ await new Promise(resolve => {
276
+ requestAnimationFrame(() => {
277
+ requestAnimationFrame(resolve);
278
+ });
279
+ });
280
+
281
+ // Additional settle time for CSS/animations
282
+ await new Promise(resolve => setTimeout(resolve, 100));
283
+
284
+ // Run axe-core accessibility audit
285
+ const results = await axe.run('#render-root', {
286
+ runOnly: {
287
+ type: 'tag',
288
+ values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'],
289
+ },
290
+ });
291
+
292
+ window.__AXE_RESULTS__ = results;
293
+ window.__RENDER_READY__ = true;
294
+ } catch (error) {
295
+ console.error("A11y audit error:", error);
296
+ window.__AXE_ERROR__ = error.message;
297
+ window.__RENDER_READY__ = true;
298
+ }
299
+ }
300
+
301
+ render();
302
+ `;
303
+ }
304
+
305
+ /**
306
+ * Generate a virtual module ID for a render request.
307
+ * This creates a unique ID that Vite can resolve.
308
+ */
309
+ export function generateRenderModuleId(componentName: string, requestId: string): string {
310
+ return `virtual:fragments-render-${componentName}-${requestId}`;
311
+ }
@@ -0,0 +1,10 @@
1
+ .accessibilityList {
2
+ margin: 0.5rem 0 0;
3
+ padding-left: 1.125rem;
4
+ font-size: 0.8125rem;
5
+ line-height: 1.6;
6
+
7
+ li {
8
+ margin-bottom: 0.25rem;
9
+ }
10
+ }
@@ -0,0 +1,2 @@
1
+ declare const styles: Record<string, string>;
2
+ export default styles;
@@ -0,0 +1,274 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import {
5
+ Badge,
6
+ Box,
7
+ Card,
8
+ CardBody,
9
+ Grid,
10
+ ListRoot,
11
+ ListItem,
12
+ Stack,
13
+ Text,
14
+ Alert,
15
+ AlertIcon,
16
+ AlertBody,
17
+ AlertTitle,
18
+ AlertContent,
19
+ CodeBlock,
20
+ Link,
21
+ } from '@fragments-sdk/ui';
22
+ import type { DocProp } from './types';
23
+ import { PropsTable } from './PropsTable';
24
+ import styles from './ComponentDocContent.module.scss';
25
+
26
+ /** Normalize PascalCase to lowercase with spaces: "AppSwitcher" → "app switcher" */
27
+ function pascalToLowerSpaced(name: string): string {
28
+ return name.replace(/([A-Z])/g, ' $1').trim().toLowerCase();
29
+ }
30
+
31
+ /** Detect auto-generated placeholder descriptions like "ActionMenu component" */
32
+ function isGenericDescription(name: string, description: string): boolean {
33
+ const lower = description.toLowerCase().trim();
34
+ const nameLower = name.toLowerCase();
35
+ const nameSpaced = pascalToLowerSpaced(name);
36
+ return (
37
+ lower === `${nameLower} component` ||
38
+ lower === `${nameSpaced} component` ||
39
+ lower === `${nameLower} component.` ||
40
+ lower === `${nameSpaced} component.` ||
41
+ (lower.startsWith('interactive ') && lower.endsWith(' element for triggering actions')) ||
42
+ (lower.startsWith('form ') && lower.endsWith(' for user input')) ||
43
+ (lower.startsWith('container ') && lower.endsWith(' for grouping content'))
44
+ );
45
+ }
46
+
47
+ /** Detect placeholder usage items like "Use X for its intended purpose" */
48
+ function isGenericUsageItem(item: string): boolean {
49
+ const lower = item.toLowerCase().trim();
50
+ return (
51
+ (lower.startsWith('use ') && lower.endsWith(' for its intended purpose')) ||
52
+ lower === 'when a more specific component is available' ||
53
+ lower.startsWith('todo:')
54
+ );
55
+ }
56
+
57
+ /** Check if any real (non-placeholder) usage content exists */
58
+ function hasRealUsageContent(usage: { when: string[]; whenNot: string[]; guidelines: string[]; accessibility: string[] }): boolean {
59
+ const allItems = [...usage.when, ...usage.whenNot, ...usage.guidelines, ...usage.accessibility];
60
+ return allItems.length > 0 && allItems.some(item => !isGenericUsageItem(item));
61
+ }
62
+
63
+ /** Filter out generic placeholder items from usage lists */
64
+ function filterRealItems(items: string[]): string[] {
65
+ return items.filter(item => !isGenericUsageItem(item));
66
+ }
67
+
68
+ interface StandardReference {
69
+ id: string;
70
+ title: string;
71
+ url: string;
72
+ }
73
+
74
+ export interface ComponentDocContentProps {
75
+ name: string;
76
+ description: string;
77
+ componentId: string;
78
+ props: Record<string, DocProp>;
79
+ variants: Array<{ name: string; description?: string; code?: string }>;
80
+ usage: {
81
+ when: string[];
82
+ whenNot: string[];
83
+ guidelines: string[];
84
+ accessibility: string[];
85
+ };
86
+ relations: Array<{ component: string; relationship: string; note?: string }>;
87
+ dependencies?: Array<{ name: string }>;
88
+ standards?: StandardReference[];
89
+
90
+ /** Custom package name for the import path (e.g. '@payroc/react'). Defaults to '@fragments-sdk/ui'. */
91
+ packageName?: string;
92
+
93
+ /** Render a variant example (framework-specific). */
94
+ renderVariant: (variant: { name: string; description?: string; code?: string }, index: number) => ReactNode;
95
+ /** Render a related-component link (framework-specific). If omitted, renders plain text. */
96
+ renderRelatedLink?: (component: string, relationship: string, note: string | undefined, key: string) => ReactNode;
97
+ }
98
+
99
+ export function ComponentDocContent({
100
+ name,
101
+ description,
102
+ componentId,
103
+ props,
104
+ variants,
105
+ usage,
106
+ relations,
107
+ packageName,
108
+ dependencies,
109
+ standards,
110
+ renderVariant,
111
+ renderRelatedLink,
112
+ }: ComponentDocContentProps) {
113
+ return (
114
+ <Stack gap="xl">
115
+ <Box as="header">
116
+ <Stack gap="sm">
117
+ <Text as="h1" size="2xl" weight="semibold">{name}</Text>
118
+ {!isGenericDescription(name, description) && (
119
+ <Text as="p" color="secondary">{description}</Text>
120
+ )}
121
+ </Stack>
122
+ </Box>
123
+
124
+ <Box as="section">
125
+ <Stack gap="md">
126
+ <Text as="h2" id="setup" size="xl" weight="semibold">Setup</Text>
127
+ <CodeBlock
128
+ code={`import { ${componentId} } from '${packageName || '@fragments-sdk/ui'}';`}
129
+ language="tsx"
130
+ />
131
+ {dependencies && dependencies.length > 0 && (
132
+ <Stack gap="sm">
133
+ <Text as="h3" size="base" weight="semibold">Dependencies</Text>
134
+ <Text size="sm" color="secondary">
135
+ This component requires additional packages:
136
+ </Text>
137
+ <CodeBlock
138
+ code={`npm install ${dependencies.map((d) => d.name).join(' ')}`}
139
+ language="bash"
140
+ />
141
+ </Stack>
142
+ )}
143
+ </Stack>
144
+ </Box>
145
+
146
+ {variants.length > 0 && (
147
+ <Box as="section">
148
+ <Stack gap="md">
149
+ <Text as="h2" id="examples" size="xl" weight="semibold">Examples</Text>
150
+ {variants.map((variant, index) => renderVariant(variant, index))}
151
+ </Stack>
152
+ </Box>
153
+ )}
154
+
155
+ {Object.keys(props).length > 0 && (
156
+ <Box as="section">
157
+ <Stack gap="md">
158
+ <Text as="h2" id="props" size="xl" weight="semibold">Props</Text>
159
+ <PropsTable props={props} />
160
+ </Stack>
161
+ </Box>
162
+ )}
163
+
164
+ {hasRealUsageContent(usage) && (
165
+ <Box as="section">
166
+ <Stack gap="md">
167
+ <Text as="h2" id="usage-guidelines" size="xl" weight="semibold">Usage Guidelines</Text>
168
+
169
+ <Grid columns={{ base: 1, md: 2 }} gap="md">
170
+ {filterRealItems(usage.when).length > 0 && (
171
+ <Box background="secondary" rounded="md" padding="md">
172
+ <Stack gap="sm">
173
+ <Text as="h3" id="when-to-use" size="base" weight="semibold">When to use</Text>
174
+ <ListRoot variant="disc" gap="xs">
175
+ {filterRealItems(usage.when).map((item, i) => (
176
+ <ListItem key={i}>{item}</ListItem>
177
+ ))}
178
+ </ListRoot>
179
+ </Stack>
180
+ </Box>
181
+ )}
182
+
183
+ {filterRealItems(usage.whenNot).length > 0 && (
184
+ <Box background="secondary" rounded="md" padding="md">
185
+ <Stack gap="sm">
186
+ <Text as="h3" id="when-not-to-use" size="base" weight="semibold">When not to use</Text>
187
+ <ListRoot variant="disc" gap="xs">
188
+ {filterRealItems(usage.whenNot).map((item, i) => (
189
+ <ListItem key={i}>{item}</ListItem>
190
+ ))}
191
+ </ListRoot>
192
+ </Stack>
193
+ </Box>
194
+ )}
195
+ </Grid>
196
+
197
+ {filterRealItems(usage.guidelines).length > 0 && (
198
+ <Stack gap="sm">
199
+ <Text as="h3" id="best-practices" size="base" weight="semibold">Best practices</Text>
200
+ <ListRoot variant="disc" gap="xs">
201
+ {filterRealItems(usage.guidelines).map((item, i) => (
202
+ <ListItem key={i}>{item}</ListItem>
203
+ ))}
204
+ </ListRoot>
205
+ </Stack>
206
+ )}
207
+
208
+ {usage.accessibility.length > 0 && (
209
+ <Alert severity="info">
210
+ <AlertIcon />
211
+ <AlertBody>
212
+ <AlertTitle>Accessibility</AlertTitle>
213
+ <AlertContent>
214
+ <ul className={styles.accessibilityList}>
215
+ {usage.accessibility.map((item, i) => (
216
+ <li key={i}>{item}</li>
217
+ ))}
218
+ </ul>
219
+ </AlertContent>
220
+ </AlertBody>
221
+ </Alert>
222
+ )}
223
+ </Stack>
224
+ </Box>
225
+ )}
226
+
227
+ {standards && standards.length > 0 && (
228
+ <Box as="section">
229
+ <Stack gap="sm">
230
+ <Text as="h2" id="standards" size="xl" weight="semibold">Standards References</Text>
231
+ <ListRoot variant="disc" gap="xs">
232
+ {standards.map((standard) => (
233
+ <ListItem key={standard.id}>
234
+ <Link href={standard.url} target="_blank" rel="noreferrer">
235
+ {standard.title}
236
+ </Link>
237
+ </ListItem>
238
+ ))}
239
+ </ListRoot>
240
+ </Stack>
241
+ </Box>
242
+ )}
243
+
244
+ {relations.length > 0 && (
245
+ <Box as="section">
246
+ <Stack gap="md">
247
+ <Text as="h2" id="related-components" size="xl" weight="semibold">Related Components</Text>
248
+ <Grid columns="auto" minChildWidth="220px" gap="sm">
249
+ {relations.map((relation) => {
250
+ const key = `${relation.component}-${relation.relationship}`;
251
+ if (renderRelatedLink) {
252
+ return renderRelatedLink(relation.component, relation.relationship, relation.note, key);
253
+ }
254
+ return (
255
+ <Card key={key} variant="outlined">
256
+ <CardBody>
257
+ <Stack gap="xs">
258
+ <Stack direction="row" align="center" justify="between">
259
+ <Text weight="semibold">{relation.component}</Text>
260
+ <Badge size="sm">{relation.relationship}</Badge>
261
+ </Stack>
262
+ {relation.note && <Text size="sm" color="secondary">{relation.note}</Text>}
263
+ </Stack>
264
+ </CardBody>
265
+ </Card>
266
+ );
267
+ })}
268
+ </Grid>
269
+ </Stack>
270
+ </Box>
271
+ )}
272
+ </Stack>
273
+ );
274
+ }