@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.
Files changed (128) hide show
  1. package/dist/bin.js +517 -77
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KFYN37ZY.js → init-2GEGVIUQ.js} +14 -76
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-HZK4BSDK.js → viewer-RFA2KVBG.js} +249 -22
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +2 -2
  36. package/src/bin.ts +26 -0
  37. package/src/build.ts +12 -2
  38. package/src/commands/build.ts +16 -2
  39. package/src/commands/doctor.ts +498 -0
  40. package/src/commands/generate.ts +383 -68
  41. package/src/commands/init-framework.ts +1 -1
  42. package/src/commands/init.ts +9 -51
  43. package/src/core/config.ts +15 -2
  44. package/src/core/generators/typescript-extractor.ts +10 -0
  45. package/src/core/index.ts +15 -0
  46. package/src/core/schema.ts +10 -2
  47. package/src/core/storyFilters.test.ts +350 -0
  48. package/src/core/storyFilters.ts +253 -0
  49. package/src/core/types.ts +22 -0
  50. package/src/migrate/converter.ts +9 -1
  51. package/src/migrate/parser.ts +2 -0
  52. package/src/migrate/types.ts +2 -0
  53. package/src/setup.ts +69 -24
  54. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  55. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  56. package/src/viewer/components/ActionsPanel.tsx +31 -29
  57. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  58. package/src/viewer/components/App.tsx +187 -740
  59. package/src/viewer/components/BottomPanel.tsx +228 -132
  60. package/src/viewer/components/CodePanel.tsx +1 -1
  61. package/src/viewer/components/CommandPalette.tsx +7 -10
  62. package/src/viewer/components/ComponentDocView.tsx +164 -0
  63. package/src/viewer/components/ComponentGraph.tsx +111 -142
  64. package/src/viewer/components/ContractPanel.tsx +6 -6
  65. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  66. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  67. package/src/viewer/components/FragmentEditor.tsx +92 -115
  68. package/src/viewer/components/HeaderSearch.tsx +24 -0
  69. package/src/viewer/components/HealthDashboard.tsx +16 -2
  70. package/src/viewer/components/Icons.tsx +9 -0
  71. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  72. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  73. package/src/viewer/components/LandingPage.tsx +3 -3
  74. package/src/viewer/components/LeftSidebar.tsx +141 -63
  75. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  76. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  77. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  78. package/src/viewer/components/PanelShell.tsx +161 -0
  79. package/src/viewer/components/PerformancePanel.tsx +31 -28
  80. package/src/viewer/components/PreviewArea.tsx +1 -1
  81. package/src/viewer/components/PreviewAside.tsx +168 -0
  82. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  83. package/src/viewer/components/PropsEditor.tsx +70 -156
  84. package/src/viewer/components/ResizablePanel.tsx +103 -263
  85. package/src/viewer/components/RightSidebar.tsx +3 -9
  86. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  87. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  88. package/src/viewer/components/TopToolbar.tsx +159 -0
  89. package/src/viewer/components/VariantMatrix.tsx +42 -86
  90. package/src/viewer/components/VariantTabs.tsx +3 -3
  91. package/src/viewer/components/ViewerHeader.tsx +69 -0
  92. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  93. package/src/viewer/components/viewer-utils.ts +16 -0
  94. package/src/viewer/entry.tsx +5 -0
  95. package/src/viewer/hooks/useAppState.ts +27 -4
  96. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  97. package/src/viewer/preview-frame.html +6 -12
  98. package/src/viewer/server.ts +184 -6
  99. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  100. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  101. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  102. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  105. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  108. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  109. package/src/viewer/vendor/shared/src/docs-data/index.ts +32 -0
  110. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +72 -0
  111. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +75 -0
  112. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +55 -0
  113. package/src/viewer/vendor/shared/src/index.ts +8 -0
  114. package/src/viewer/vendor/shared/src/types.ts +12 -0
  115. package/src/viewer/vite-plugin.ts +109 -4
  116. package/dist/chunk-2JIKCJX3.js.map +0 -1
  117. package/dist/chunk-CJEGT3WD.js.map +0 -1
  118. package/dist/chunk-GOVI6COW.js.map +0 -1
  119. package/dist/generate-35OIMW4Y.js +0 -252
  120. package/dist/generate-35OIMW4Y.js.map +0 -1
  121. package/dist/init-KFYN37ZY.js.map +0 -1
  122. package/dist/viewer-HZK4BSDK.js.map +0 -1
  123. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  124. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  125. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  126. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  127. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  128. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -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, and node_modules root
283
- allow: [viewerRoot, uiLibRoot, sharedLibRoot, webmcpLibRoot, contextLibRoot, projectRoot, configDir, dirname(nodeModulesPath), ...installedPkgRoots],
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": join(webmcpLibRoot, "react/index.ts"),
326
- "@fragments-sdk/webmcp/fragments": join(webmcpLibRoot, "fragments/index.ts"),
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": join(contextLibRoot, "types/index.ts"),
330
- "@fragments-sdk/context/mcp-tools": join(contextLibRoot, "mcp-tools/index.ts"),
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,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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ declare const styles: Record<string, string>;
2
+ export default styles;