@fragments-sdk/cli 0.9.0 → 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 (123) hide show
  1. package/dist/bin.js +83 -33
  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-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
  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-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/build.ts +12 -2
  37. package/src/commands/build.ts +16 -2
  38. package/src/commands/generate.ts +383 -68
  39. package/src/commands/init.ts +9 -51
  40. package/src/core/config.ts +15 -2
  41. package/src/core/generators/typescript-extractor.ts +10 -0
  42. package/src/core/index.ts +15 -0
  43. package/src/core/schema.ts +10 -2
  44. package/src/core/storyFilters.test.ts +350 -0
  45. package/src/core/storyFilters.ts +253 -0
  46. package/src/core/types.ts +22 -0
  47. package/src/migrate/converter.ts +9 -1
  48. package/src/migrate/parser.ts +2 -0
  49. package/src/migrate/types.ts +2 -0
  50. package/src/setup.ts +69 -24
  51. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  52. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  53. package/src/viewer/components/ActionsPanel.tsx +31 -29
  54. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  55. package/src/viewer/components/App.tsx +187 -740
  56. package/src/viewer/components/BottomPanel.tsx +228 -132
  57. package/src/viewer/components/CodePanel.tsx +1 -1
  58. package/src/viewer/components/CommandPalette.tsx +7 -10
  59. package/src/viewer/components/ComponentDocView.tsx +164 -0
  60. package/src/viewer/components/ComponentGraph.tsx +111 -142
  61. package/src/viewer/components/ContractPanel.tsx +6 -6
  62. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  63. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  64. package/src/viewer/components/FragmentEditor.tsx +92 -115
  65. package/src/viewer/components/HeaderSearch.tsx +24 -0
  66. package/src/viewer/components/HealthDashboard.tsx +16 -2
  67. package/src/viewer/components/Icons.tsx +9 -0
  68. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  69. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  70. package/src/viewer/components/LandingPage.tsx +3 -3
  71. package/src/viewer/components/LeftSidebar.tsx +141 -63
  72. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  73. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  74. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  75. package/src/viewer/components/PanelShell.tsx +161 -0
  76. package/src/viewer/components/PerformancePanel.tsx +31 -28
  77. package/src/viewer/components/PreviewArea.tsx +1 -1
  78. package/src/viewer/components/PreviewAside.tsx +168 -0
  79. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  80. package/src/viewer/components/PropsEditor.tsx +70 -156
  81. package/src/viewer/components/ResizablePanel.tsx +103 -263
  82. package/src/viewer/components/RightSidebar.tsx +3 -9
  83. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  84. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  85. package/src/viewer/components/TopToolbar.tsx +159 -0
  86. package/src/viewer/components/VariantMatrix.tsx +42 -86
  87. package/src/viewer/components/VariantTabs.tsx +3 -3
  88. package/src/viewer/components/ViewerHeader.tsx +69 -0
  89. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  90. package/src/viewer/components/viewer-utils.ts +16 -0
  91. package/src/viewer/entry.tsx +5 -0
  92. package/src/viewer/hooks/useAppState.ts +27 -4
  93. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  94. package/src/viewer/preview-frame.html +6 -12
  95. package/src/viewer/server.ts +169 -2
  96. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  97. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  98. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  99. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  100. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  101. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  102. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  105. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  108. package/src/viewer/vendor/shared/src/index.ts +8 -0
  109. package/src/viewer/vendor/shared/src/types.ts +12 -0
  110. package/src/viewer/vite-plugin.ts +109 -4
  111. package/dist/chunk-2JIKCJX3.js.map +0 -1
  112. package/dist/chunk-CJEGT3WD.js.map +0 -1
  113. package/dist/chunk-GOVI6COW.js.map +0 -1
  114. package/dist/generate-35OIMW4Y.js +0 -252
  115. package/dist/generate-35OIMW4Y.js.map +0 -1
  116. package/dist/init-KSAAS7X3.js.map +0 -1
  117. package/dist/viewer-SBTJDMP7.js.map +0 -1
  118. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  119. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  120. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  121. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  122. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  123. /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;
@@ -258,6 +391,19 @@ export async function createDevServer(
258
391
  const isContextSource = existsSync(join(contextLibRoot, "types/index.ts"));
259
392
  console.log(`📁 Using node_modules: ${nodeModulesPath}`);
260
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
+
261
407
  // Collect installed package roots so Vite can serve files from node_modules
262
408
  const installedPkgRoots = [...new Set(
263
409
  installedFiles.map(f => {
@@ -282,8 +428,13 @@ export async function createDevServer(
282
428
  port,
283
429
  open: open ? "/fragments/" : false,
284
430
  fs: {
285
- // Allow serving files from viewer package, project, shared libs, and node_modules root
286
- 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
+ ],
287
438
  },
288
439
  },
289
440
 
@@ -294,6 +445,9 @@ export async function createDevServer(
294
445
  // CJS interop for packages imported from within node_modules
295
446
  cjsInteropPlugin(nodeModulesPath),
296
447
 
448
+ // Rewrite require() → ESM import for optional peer deps (e.g., @tanstack/react-table)
449
+ optionalPeerDepsPlugin(uiLibRoot),
450
+
297
451
  // Fragments plugins (array including SVGR)
298
452
  ...fragmentsPlugin({
299
453
  fragmentFiles: allFragmentFiles,
@@ -308,6 +462,15 @@ export async function createDevServer(
308
462
  modules: {
309
463
  localsConvention: 'camelCase',
310
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
+ },
311
474
  },
312
475
 
313
476
  optimizeDeps: {
@@ -320,6 +483,10 @@ export async function createDevServer(
320
483
  // Dedupe ensures all imports of these packages resolve to the same copy
321
484
  dedupe: ["react", "react-dom"],
322
485
  alias: {
486
+ // Project-specific aliases (tsconfig paths, Storybook static dirs)
487
+ // Listed first — Fragments-specific aliases below take precedence
488
+ ...tsconfigAliases,
489
+ ...storybookAliases,
323
490
  // Resolve @fragments-sdk/ui to local source or installed package
324
491
  "@fragments-sdk/ui": uiLibRoot,
325
492
  // Resolve @fragments-sdk/shared to monorepo source or vendored fallback
@@ -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
+ }
@@ -20,8 +20,6 @@ interface DocsHeaderBarProps {
20
20
  navAriaLabel?: string;
21
21
  }
22
22
 
23
- const EMPTY_SECTIONS: NavSection[] = [];
24
-
25
23
  const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
26
24
  <a href={href} onClick={onClick}>
27
25
  {label}
@@ -32,20 +30,10 @@ function defaultIsActive(href: string, currentPath: string): boolean {
32
30
  return currentPath === href || currentPath.startsWith(`${href}/`);
33
31
  }
34
32
 
35
- /** Named component wrapper to avoid inline render functions in JSX */
36
- function NavLink({ renderer, href, label, onClick }: {
37
- renderer: DocsNavLinkRenderer;
38
- href: string;
39
- label: string;
40
- onClick?: () => void;
41
- }) {
42
- return <>{renderer({ href, label, onClick })}</>;
43
- }
44
-
45
33
  export function DocsHeaderBar({
46
34
  brand,
47
35
  headerNav,
48
- mobileSections = EMPTY_SECTIONS,
36
+ mobileSections = [],
49
37
  currentPath,
50
38
  searchItems,
51
39
  onSearchSelect,
@@ -75,7 +63,7 @@ export function DocsHeaderBar({
75
63
  active={isActive(child.href, currentPath)}
76
64
  asChild
77
65
  >
78
- <NavLink renderer={renderLink} href={child.href} label={child.label} />
66
+ {renderLink({ href: child.href, label: child.label })}
79
67
  </NavigationMenu.Link>
80
68
  ))}
81
69
  </div>
@@ -88,7 +76,7 @@ export function DocsHeaderBar({
88
76
  active={isActive(entry.href, currentPath)}
89
77
  asChild
90
78
  >
91
- <NavLink renderer={renderLink} href={entry.href} label={entry.label} />
79
+ {renderLink({ href: entry.href, label: entry.label })}
92
80
  </NavigationMenu.Link>
93
81
  </NavigationMenu.Item>
94
82
  )
@@ -106,12 +94,12 @@ export function DocsHeaderBar({
106
94
  isDropdown(entry) ? (
107
95
  entry.items.map((child) => (
108
96
  <NavigationMenu.Link key={child.href} href={child.href} asChild>
109
- <NavLink renderer={renderLink} href={child.href} label={child.label} />
97
+ {renderLink({ href: child.href, label: child.label })}
110
98
  </NavigationMenu.Link>
111
99
  ))
112
100
  ) : (
113
101
  <NavigationMenu.Link key={entry.href} href={entry.href} asChild>
114
- <NavLink renderer={renderLink} href={entry.href} label={entry.label} />
102
+ {renderLink({ href: entry.href, label: entry.label })}
115
103
  </NavigationMenu.Link>
116
104
  )
117
105
  )}
@@ -121,7 +109,7 @@ export function DocsHeaderBar({
121
109
  <NavigationMenu.MobileSection key={section.title} label={section.title}>
122
110
  {section.items.map((item) => (
123
111
  <NavigationMenu.Link key={item.href} href={item.href} asChild>
124
- <NavLink renderer={renderLink} href={item.href} label={item.label} />
112
+ {renderLink({ href: item.href, label: item.label })}
125
113
  </NavigationMenu.Link>
126
114
  ))}
127
115
  </NavigationMenu.MobileSection>
@@ -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}
@@ -24,16 +24,6 @@ const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
24
24
  </a>
25
25
  );
26
26
 
27
- /** Named component wrapper to avoid inline render functions in JSX */
28
- function SidebarLink({ renderer, href, label, onClick }: {
29
- renderer: DocsNavLinkRenderer;
30
- href: string;
31
- label: string;
32
- onClick?: () => void;
33
- }) {
34
- return <>{renderer({ href, label, onClick })}</>;
35
- }
36
-
37
27
  export function DocsSidebarNav({
38
28
  sections,
39
29
  currentPath,
@@ -59,12 +49,11 @@ export function DocsSidebarNav({
59
49
  active={isActive(item.href, currentPath)}
60
50
  asChild
61
51
  >
62
- <SidebarLink
63
- renderer={renderLink}
64
- href={item.href}
65
- label={item.label}
66
- onClick={onNavigate}
67
- />
52
+ {renderLink({
53
+ href: item.href,
54
+ label: item.label,
55
+ onClick: onNavigate,
56
+ })}
68
57
  </Sidebar.Item>
69
58
  ))}
70
59
  </Sidebar.Section>