@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,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,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
|
+
}
|