@fragments-sdk/cli 0.8.1 → 0.9.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/dist/bin.js +517 -77
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
- package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
- package/dist/chunk-BW3ZATBW.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
- package/dist/chunk-D7372LQX.js.map +1 -0
- package/dist/chunk-EZYXYWNF.js +131 -0
- package/dist/chunk-EZYXYWNF.js.map +1 -0
- package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
- package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
- package/dist/chunk-NVSPGSKB.js.map +1 -0
- package/dist/core/index.d.ts +105 -3
- package/dist/core/index.js +12 -2
- package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
- package/dist/generate-LQA2R7FN.js +461 -0
- package/dist/generate-LQA2R7FN.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
- package/dist/init-2GEGVIUQ.js.map +1 -0
- package/dist/mcp-bin.js +4 -3
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
- package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
- package/dist/storyFilters-3LUYAFZF.js +15 -0
- package/dist/storyFilters-3LUYAFZF.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
- package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
- package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
- package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
- package/dist/viewer-RFA2KVBG.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +26 -0
- package/src/build.ts +12 -2
- package/src/commands/build.ts +16 -2
- package/src/commands/doctor.ts +498 -0
- package/src/commands/generate.ts +383 -68
- package/src/commands/init-framework.ts +1 -1
- package/src/commands/init.ts +9 -51
- package/src/core/config.ts +15 -2
- package/src/core/generators/typescript-extractor.ts +10 -0
- package/src/core/index.ts +15 -0
- package/src/core/schema.ts +10 -2
- package/src/core/storyFilters.test.ts +350 -0
- package/src/core/storyFilters.ts +253 -0
- package/src/core/types.ts +22 -0
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
- package/src/viewer/components/AccessibilityPanel.tsx +305 -312
- package/src/viewer/components/ActionsPanel.tsx +31 -29
- package/src/viewer/components/AllVariantsPreview.tsx +78 -0
- package/src/viewer/components/App.tsx +187 -740
- package/src/viewer/components/BottomPanel.tsx +228 -132
- package/src/viewer/components/CodePanel.tsx +1 -1
- package/src/viewer/components/CommandPalette.tsx +7 -10
- package/src/viewer/components/ComponentDocView.tsx +164 -0
- package/src/viewer/components/ComponentGraph.tsx +111 -142
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
- package/src/viewer/components/FigmaEmbed.tsx +20 -18
- package/src/viewer/components/FragmentEditor.tsx +92 -115
- package/src/viewer/components/HeaderSearch.tsx +24 -0
- package/src/viewer/components/HealthDashboard.tsx +16 -2
- package/src/viewer/components/Icons.tsx +9 -0
- package/src/viewer/components/InteractionsPanel.tsx +101 -117
- package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
- package/src/viewer/components/LandingPage.tsx +3 -3
- package/src/viewer/components/LeftSidebar.tsx +141 -63
- package/src/viewer/components/LoadErrorMessage.tsx +102 -0
- package/src/viewer/components/MultiViewportPreview.tsx +61 -142
- package/src/viewer/components/NoVariantsMessage.tsx +59 -0
- package/src/viewer/components/PanelShell.tsx +161 -0
- package/src/viewer/components/PerformancePanel.tsx +31 -28
- package/src/viewer/components/PreviewArea.tsx +1 -1
- package/src/viewer/components/PreviewAside.tsx +168 -0
- package/src/viewer/components/PreviewFrameHost.tsx +3 -3
- package/src/viewer/components/PropsEditor.tsx +70 -156
- package/src/viewer/components/ResizablePanel.tsx +103 -263
- package/src/viewer/components/RightSidebar.tsx +3 -9
- package/src/viewer/components/SkeletonLoader.tsx +13 -13
- package/src/viewer/components/TokenStylePanel.tsx +182 -209
- package/src/viewer/components/TopToolbar.tsx +159 -0
- package/src/viewer/components/VariantMatrix.tsx +42 -86
- package/src/viewer/components/VariantTabs.tsx +3 -3
- package/src/viewer/components/ViewerHeader.tsx +69 -0
- package/src/viewer/components/WebMCPDevTools.tsx +17 -23
- package/src/viewer/components/viewer-utils.ts +16 -0
- package/src/viewer/entry.tsx +5 -0
- package/src/viewer/hooks/useAppState.ts +27 -4
- package/src/viewer/hooks/usePreviewBridge.ts +2 -2
- package/src/viewer/preview-frame.html +6 -12
- package/src/viewer/server.ts +184 -6
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
- package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
- package/src/viewer/vendor/shared/src/index.ts +8 -0
- package/src/viewer/vendor/shared/src/types.ts +12 -0
- package/src/viewer/vite-plugin.ts +109 -4
- package/dist/chunk-2JIKCJX3.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KFYN37ZY.js.map +0 -1
- package/dist/viewer-HZK4BSDK.js.map +0 -1
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
- /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
- /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
package/src/viewer/server.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import react from "@vitejs/plugin-react";
|
|
21
21
|
import { resolve, dirname, join } from "node:path";
|
|
22
22
|
import { existsSync, realpathSync } from "node:fs";
|
|
23
|
+
import { readFile } from "node:fs/promises";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
24
25
|
import { loadConfig, discoverFragmentFiles, discoverInstalledFragments } from "../core/node.js";
|
|
25
26
|
import { fragmentsPlugin } from "./vite-plugin.js";
|
|
@@ -183,6 +184,138 @@ export function useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServ
|
|
|
183
184
|
};
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Vite plugin to rewrite CJS require() calls for optional peer dependencies
|
|
189
|
+
* into ESM imports. UI components use lazy require() inside try-catch for
|
|
190
|
+
* graceful degradation when optional deps aren't installed. But in the browser
|
|
191
|
+
* (Vite dev mode), require() doesn't exist — esbuild preserves it inside
|
|
192
|
+
* try-catch blocks, so it always throws and components return null.
|
|
193
|
+
*
|
|
194
|
+
* This plugin detects which optional peer deps are actually installed and
|
|
195
|
+
* rewrites require('dep') → static ESM import reference in component source.
|
|
196
|
+
*/
|
|
197
|
+
function optionalPeerDepsPlugin(uiLibRoot: string) {
|
|
198
|
+
// Optional peer deps from @fragments-sdk/ui that use lazy require()
|
|
199
|
+
const optionalDeps = [
|
|
200
|
+
'@tanstack/react-table',
|
|
201
|
+
'shiki',
|
|
202
|
+
'recharts',
|
|
203
|
+
'react-day-picker',
|
|
204
|
+
'date-fns',
|
|
205
|
+
'react-colorful',
|
|
206
|
+
'react-markdown',
|
|
207
|
+
'remark-gfm',
|
|
208
|
+
'@tiptap/react',
|
|
209
|
+
'@tiptap/starter-kit',
|
|
210
|
+
'@tiptap/extension-link',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
// Resolve the real path for symlink-safe comparison (pnpm uses symlinks)
|
|
214
|
+
let resolvedUiRoot: string;
|
|
215
|
+
try {
|
|
216
|
+
resolvedUiRoot = realpathSync(uiLibRoot);
|
|
217
|
+
} catch {
|
|
218
|
+
resolvedUiRoot = uiLibRoot;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check which deps are actually installed (pnpm puts them in the package's node_modules)
|
|
222
|
+
const availableDeps = new Set<string>();
|
|
223
|
+
for (const dep of optionalDeps) {
|
|
224
|
+
const depPath = join(uiLibRoot, '..', 'node_modules', ...dep.split('/'));
|
|
225
|
+
if (existsSync(depPath)) availableDeps.add(dep);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
name: 'fragments:optional-peer-deps',
|
|
230
|
+
enforce: 'pre' as const,
|
|
231
|
+
transform(code: string, id: string) {
|
|
232
|
+
// Only transform .tsx/.ts files from the UI lib source
|
|
233
|
+
let resolvedId: string;
|
|
234
|
+
try {
|
|
235
|
+
resolvedId = realpathSync(id);
|
|
236
|
+
} catch {
|
|
237
|
+
resolvedId = id;
|
|
238
|
+
}
|
|
239
|
+
if (!resolvedId.startsWith(resolvedUiRoot)) return;
|
|
240
|
+
if (!id.endsWith('.tsx') && !id.endsWith('.ts')) return;
|
|
241
|
+
if (!code.includes('require(')) return;
|
|
242
|
+
|
|
243
|
+
let transformed = code;
|
|
244
|
+
const imports: string[] = [];
|
|
245
|
+
let counter = 0;
|
|
246
|
+
|
|
247
|
+
for (const dep of availableDeps) {
|
|
248
|
+
const escapedDep = dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
249
|
+
const regex = new RegExp(`require\\(['"]${escapedDep}['"]\\)`, 'g');
|
|
250
|
+
|
|
251
|
+
if (regex.test(code)) {
|
|
252
|
+
const varName = `__fui_dep_${counter++}`;
|
|
253
|
+
imports.push(`import * as ${varName} from '${dep}';`);
|
|
254
|
+
// Reset lastIndex after test() since regex has 'g' flag
|
|
255
|
+
regex.lastIndex = 0;
|
|
256
|
+
transformed = transformed.replace(regex, varName);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (imports.length > 0) {
|
|
261
|
+
transformed = imports.join('\n') + '\n' + transformed;
|
|
262
|
+
return { code: transformed, map: null };
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Auto-detect tsconfig.json paths and convert to Vite-compatible aliases.
|
|
270
|
+
* Handles JSON with comments (common in tsconfig files).
|
|
271
|
+
*/
|
|
272
|
+
async function detectTsconfigPaths(projectRoot: string): Promise<Record<string, string>> {
|
|
273
|
+
const aliases: Record<string, string> = {};
|
|
274
|
+
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
275
|
+
if (!existsSync(tsconfigPath)) return aliases;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const content = await readFile(tsconfigPath, 'utf-8');
|
|
279
|
+
// Strip JSON comments (single-line and multi-line)
|
|
280
|
+
const cleaned = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
281
|
+
const tsconfig = JSON.parse(cleaned);
|
|
282
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
283
|
+
const baseUrl = tsconfig?.compilerOptions?.baseUrl || '.';
|
|
284
|
+
|
|
285
|
+
if (paths) {
|
|
286
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
287
|
+
const target = (targets as string[])[0];
|
|
288
|
+
if (target) {
|
|
289
|
+
const cleanAlias = alias.replace('/*', '');
|
|
290
|
+
const cleanTarget = target.replace('/*', '');
|
|
291
|
+
aliases[cleanAlias] = resolve(projectRoot, baseUrl, cleanTarget);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
// Ignore parse errors — tsconfig may be invalid or use unsupported syntax
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return aliases;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Detect common static asset directories that Storybook projects may reference.
|
|
304
|
+
* Returns Vite-compatible aliases for serving fonts and assets.
|
|
305
|
+
*/
|
|
306
|
+
async function detectStorybookAliases(projectRoot: string): Promise<Record<string, string>> {
|
|
307
|
+
const aliases: Record<string, string> = {};
|
|
308
|
+
const assetsDir = resolve(projectRoot, 'assets');
|
|
309
|
+
const fontsDir = resolve(projectRoot, 'assets/fonts');
|
|
310
|
+
if (existsSync(fontsDir)) {
|
|
311
|
+
aliases['/fonts'] = fontsDir;
|
|
312
|
+
}
|
|
313
|
+
if (existsSync(assetsDir)) {
|
|
314
|
+
aliases['/assets'] = assetsDir;
|
|
315
|
+
}
|
|
316
|
+
return aliases;
|
|
317
|
+
}
|
|
318
|
+
|
|
186
319
|
export interface DevServerOptions {
|
|
187
320
|
/** Port to run the server on */
|
|
188
321
|
port?: number;
|
|
@@ -253,8 +386,24 @@ export async function createDevServer(
|
|
|
253
386
|
const sharedLibRoot = resolveSharedLib();
|
|
254
387
|
const webmcpLibRoot = resolveWebMCPLib(nodeModulesPath);
|
|
255
388
|
const contextLibRoot = resolveContextLib(nodeModulesPath);
|
|
389
|
+
// Detect whether resolved libs point to source (.ts) or dist (.js)
|
|
390
|
+
const isWebMCPSource = existsSync(join(webmcpLibRoot, "react/index.ts"));
|
|
391
|
+
const isContextSource = existsSync(join(contextLibRoot, "types/index.ts"));
|
|
256
392
|
console.log(`📁 Using node_modules: ${nodeModulesPath}`);
|
|
257
393
|
|
|
394
|
+
// Auto-detect tsconfig paths and Storybook aliases for external projects
|
|
395
|
+
const tsconfigAliases = await detectTsconfigPaths(projectRoot);
|
|
396
|
+
const storybookAliases = await detectStorybookAliases(projectRoot);
|
|
397
|
+
const storybookDir = resolve(projectRoot, '.storybook');
|
|
398
|
+
const hasStorybookDir = existsSync(storybookDir);
|
|
399
|
+
|
|
400
|
+
if (Object.keys(tsconfigAliases).length > 0) {
|
|
401
|
+
console.log(`📎 Detected ${Object.keys(tsconfigAliases).length} tsconfig path alias(es)`);
|
|
402
|
+
}
|
|
403
|
+
if (hasStorybookDir) {
|
|
404
|
+
console.log(`📘 Detected .storybook directory`);
|
|
405
|
+
}
|
|
406
|
+
|
|
258
407
|
// Collect installed package roots so Vite can serve files from node_modules
|
|
259
408
|
const installedPkgRoots = [...new Set(
|
|
260
409
|
installedFiles.map(f => {
|
|
@@ -279,8 +428,13 @@ export async function createDevServer(
|
|
|
279
428
|
port,
|
|
280
429
|
open: open ? "/fragments/" : false,
|
|
281
430
|
fs: {
|
|
282
|
-
// Allow serving files from viewer package, project, shared libs,
|
|
283
|
-
allow: [
|
|
431
|
+
// Allow serving files from viewer package, project, shared libs, node_modules root, and .storybook
|
|
432
|
+
allow: [
|
|
433
|
+
viewerRoot, uiLibRoot, sharedLibRoot, webmcpLibRoot, contextLibRoot,
|
|
434
|
+
projectRoot, configDir, dirname(nodeModulesPath),
|
|
435
|
+
...(hasStorybookDir ? [storybookDir] : []),
|
|
436
|
+
...installedPkgRoots,
|
|
437
|
+
],
|
|
284
438
|
},
|
|
285
439
|
},
|
|
286
440
|
|
|
@@ -291,6 +445,9 @@ export async function createDevServer(
|
|
|
291
445
|
// CJS interop for packages imported from within node_modules
|
|
292
446
|
cjsInteropPlugin(nodeModulesPath),
|
|
293
447
|
|
|
448
|
+
// Rewrite require() → ESM import for optional peer deps (e.g., @tanstack/react-table)
|
|
449
|
+
optionalPeerDepsPlugin(uiLibRoot),
|
|
450
|
+
|
|
294
451
|
// Fragments plugins (array including SVGR)
|
|
295
452
|
...fragmentsPlugin({
|
|
296
453
|
fragmentFiles: allFragmentFiles,
|
|
@@ -305,6 +462,15 @@ export async function createDevServer(
|
|
|
305
462
|
modules: {
|
|
306
463
|
localsConvention: 'camelCase',
|
|
307
464
|
},
|
|
465
|
+
preprocessorOptions: {
|
|
466
|
+
scss: {
|
|
467
|
+
api: 'modern-compiler' as const,
|
|
468
|
+
loadPaths: [
|
|
469
|
+
resolve(projectRoot, 'src'),
|
|
470
|
+
resolve(projectRoot, 'src/styles'),
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
308
474
|
},
|
|
309
475
|
|
|
310
476
|
optimizeDeps: {
|
|
@@ -317,17 +483,29 @@ export async function createDevServer(
|
|
|
317
483
|
// Dedupe ensures all imports of these packages resolve to the same copy
|
|
318
484
|
dedupe: ["react", "react-dom"],
|
|
319
485
|
alias: {
|
|
486
|
+
// Project-specific aliases (tsconfig paths, Storybook static dirs)
|
|
487
|
+
// Listed first — Fragments-specific aliases below take precedence
|
|
488
|
+
...tsconfigAliases,
|
|
489
|
+
...storybookAliases,
|
|
320
490
|
// Resolve @fragments-sdk/ui to local source or installed package
|
|
321
491
|
"@fragments-sdk/ui": uiLibRoot,
|
|
322
492
|
// Resolve @fragments-sdk/shared to monorepo source or vendored fallback
|
|
323
493
|
"@fragments-sdk/shared": sharedLibRoot,
|
|
324
494
|
// Resolve @fragments-sdk/webmcp subpaths to monorepo source or installed package
|
|
325
|
-
"@fragments-sdk/webmcp/react":
|
|
326
|
-
|
|
495
|
+
"@fragments-sdk/webmcp/react": isWebMCPSource
|
|
496
|
+
? join(webmcpLibRoot, "react/index.ts")
|
|
497
|
+
: join(webmcpLibRoot, "dist/react/index.js"),
|
|
498
|
+
"@fragments-sdk/webmcp/fragments": isWebMCPSource
|
|
499
|
+
? join(webmcpLibRoot, "fragments/index.ts")
|
|
500
|
+
: join(webmcpLibRoot, "dist/fragments/index.js"),
|
|
327
501
|
"@fragments-sdk/webmcp": webmcpLibRoot,
|
|
328
502
|
// Resolve @fragments-sdk/context subpaths to monorepo source or installed package
|
|
329
|
-
"@fragments-sdk/context/types":
|
|
330
|
-
|
|
503
|
+
"@fragments-sdk/context/types": isContextSource
|
|
504
|
+
? join(contextLibRoot, "types/index.ts")
|
|
505
|
+
: join(contextLibRoot, "dist/types/index.js"),
|
|
506
|
+
"@fragments-sdk/context/mcp-tools": isContextSource
|
|
507
|
+
? join(contextLibRoot, "mcp-tools/index.ts")
|
|
508
|
+
: join(contextLibRoot, "dist/mcp-tools/index.js"),
|
|
331
509
|
"@fragments-sdk/context": contextLibRoot,
|
|
332
510
|
// Resolve @fragments-sdk/cli/core to the CLI's own core source
|
|
333
511
|
"@fragments-sdk/cli/core": resolve(cliPackageRoot, "src/core/index.ts"),
|
|
@@ -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
|
+
}
|
|
@@ -12,6 +12,7 @@ interface DocsPageShellProps {
|
|
|
12
12
|
children: ReactNode;
|
|
13
13
|
sidebarWidth?: string;
|
|
14
14
|
sidebarCollapsible?: SidebarCollapsible;
|
|
15
|
+
sidebarDefaultCollapsed?: boolean;
|
|
15
16
|
sidebarAriaLabel?: string;
|
|
16
17
|
mainId?: string;
|
|
17
18
|
mainAriaLabel?: string;
|
|
@@ -28,6 +29,7 @@ function DocsPageShellInner({
|
|
|
28
29
|
children,
|
|
29
30
|
sidebarWidth = '260px',
|
|
30
31
|
sidebarCollapsible = 'offcanvas',
|
|
32
|
+
sidebarDefaultCollapsed,
|
|
31
33
|
sidebarAriaLabel = 'Documentation sidebar',
|
|
32
34
|
mainId = 'main-content',
|
|
33
35
|
mainAriaLabel = 'Documentation content',
|
|
@@ -44,6 +46,7 @@ function DocsPageShellInner({
|
|
|
44
46
|
<AppShell.Sidebar
|
|
45
47
|
width={sidebarWidth}
|
|
46
48
|
collapsible={sidebarCollapsible}
|
|
49
|
+
defaultCollapsed={sidebarDefaultCollapsed}
|
|
47
50
|
aria-label={sidebarAriaLabel}
|
|
48
51
|
>
|
|
49
52
|
{sidebar}
|
|
@@ -70,6 +73,7 @@ function DocsPageShellPortalInner({
|
|
|
70
73
|
children,
|
|
71
74
|
sidebarWidth = '260px',
|
|
72
75
|
sidebarCollapsible = 'offcanvas',
|
|
76
|
+
sidebarDefaultCollapsed,
|
|
73
77
|
sidebarAriaLabel = 'Documentation sidebar',
|
|
74
78
|
mainId = 'main-content',
|
|
75
79
|
mainAriaLabel = 'Documentation content',
|
|
@@ -85,6 +89,7 @@ function DocsPageShellPortalInner({
|
|
|
85
89
|
<AppShell.Sidebar
|
|
86
90
|
width={sidebarWidth}
|
|
87
91
|
collapsible={sidebarCollapsible}
|
|
92
|
+
defaultCollapsed={sidebarDefaultCollapsed}
|
|
88
93
|
aria-label={sidebarAriaLabel}
|
|
89
94
|
>
|
|
90
95
|
{sidebar}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
margin: 1.5rem 0;
|
|
3
|
+
overflow-x: auto;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.empty {
|
|
7
|
+
color: var(--fui-text-tertiary);
|
|
8
|
+
font-style: italic;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.table {
|
|
12
|
+
width: 100%;
|
|
13
|
+
border-collapse: collapse;
|
|
14
|
+
font-size: 0.875rem;
|
|
15
|
+
|
|
16
|
+
th, td {
|
|
17
|
+
text-align: left;
|
|
18
|
+
padding: 0.75rem;
|
|
19
|
+
border-bottom: 1px solid var(--fui-border);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
th {
|
|
23
|
+
font-weight: var(--fui-font-weight-semibold);
|
|
24
|
+
color: var(--fui-text-secondary);
|
|
25
|
+
background-color: var(--fui-bg-secondary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
td {
|
|
29
|
+
vertical-align: top;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.propName {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 0.5rem;
|
|
37
|
+
|
|
38
|
+
code {
|
|
39
|
+
font-weight: var(--fui-font-weight-semibold);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.type {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 0.25rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.values {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
gap: 0.25rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.value {
|
|
56
|
+
font-size: 0.75rem;
|
|
57
|
+
background-color: var(--fui-bg-tertiary);
|
|
58
|
+
padding: 0.125rem 0.375rem;
|
|
59
|
+
border-radius: var(--fui-radius-sm);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.default {
|
|
63
|
+
color: var(--fui-color-accent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.noDefault {
|
|
67
|
+
color: var(--fui-text-tertiary);
|
|
68
|
+
}
|