@fragments-sdk/cli 0.9.0 → 0.10.0
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.d.ts +1 -0
- package/dist/bin.js +502 -84
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CJEGT3WD.js → chunk-566BNPQZ.js} +21 -6
- package/dist/chunk-566BNPQZ.js.map +1 -0
- package/dist/{chunk-WI6SLMSO.js → chunk-CAMXG5HJ.js} +5 -5
- package/dist/chunk-D2CDBRNU.js +2 -0
- package/dist/{chunk-YMPGYEWK.js → chunk-D5PYOXEI.js} +2 -2
- package/dist/{chunk-NGIMCIK2.js → chunk-OQO55NKV.js} +405 -34
- package/dist/chunk-OQO55NKV.js.map +1 -0
- package/dist/{chunk-TOIE7VXF.js → chunk-PW7QTQA6.js} +2 -2
- package/dist/{chunk-AWYCDRPG.js → chunk-WXSR2II7.js} +2 -2
- package/dist/chunk-WXSR2II7.js.map +1 -0
- package/dist/{chunk-2JIKCJX3.js → chunk-ZDA3PLQ6.js} +17 -14
- package/dist/chunk-ZDA3PLQ6.js.map +1 -0
- package/dist/core/index.d.ts +1 -2092
- package/dist/core/index.js +26 -21
- package/dist/{discovery-Z4RDDFVR.js → discovery-NEOY4MPN.js} +3 -3
- package/dist/generate-BGKTKO6E.js +459 -0
- package/dist/generate-BGKTKO6E.js.map +1 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/dist/{init-KSAAS7X3.js → init-Q53R5Q2T.js} +66 -76
- package/dist/init-Q53R5Q2T.js.map +1 -0
- package/dist/mcp-bin.js +5 -7
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-OQU7M4GH.js +14 -0
- package/dist/scan-generate-T5QNUG7N.js +691 -0
- package/dist/scan-generate-T5QNUG7N.js.map +1 -0
- package/dist/{service-A5GIGGGK.js → service-TQYWY65E.js} +4 -5
- package/dist/{static-viewer-NSODM5VX.js → static-viewer-NUBFPKWH.js} +4 -5
- package/dist/static-viewer-NUBFPKWH.js.map +1 -0
- package/dist/{test-RPWZAYSJ.js → test-2CSOSS3B.js} +4 -5
- package/dist/{test-RPWZAYSJ.js.map → test-2CSOSS3B.js.map} +1 -1
- package/dist/{tokens-NIXSZRX7.js → tokens-DXEGYTOJ.js} +6 -7
- package/dist/{tokens-NIXSZRX7.js.map → tokens-DXEGYTOJ.js.map} +1 -1
- package/dist/{viewer-SBTJDMP7.js → viewer-DBEPYM3G.js} +245 -23
- package/dist/viewer-DBEPYM3G.js.map +1 -0
- package/package.json +2 -1
- package/src/bin.ts +33 -1
- package/src/build.ts +13 -3
- package/src/commands/__tests__/scan-generate.test.ts +308 -0
- package/src/commands/build.ts +16 -2
- package/src/commands/generate.ts +383 -68
- package/src/commands/init.ts +81 -56
- package/src/commands/perf.ts +1 -1
- package/src/commands/scan-generate.ts +1013 -0
- package/src/commands/setup.ts +499 -0
- package/src/core/auto-props.ts +1 -1
- package/src/core/bundle-measurer.ts +2 -2
- package/src/core/config.ts +16 -4
- package/src/core/discovery.ts +2 -2
- package/src/core/generators/context.ts +1 -1
- package/src/core/generators/registry.ts +3 -3
- package/src/core/generators/typescript-extractor.ts +11 -1
- package/src/core/graph-extractor.ts +1 -1
- package/src/core/index.ts +3 -190
- package/src/core/loader.ts +2 -2
- package/src/core/parser.ts +1 -1
- package/src/core/previewLoader.ts +1 -1
- package/src/index.ts +2 -2
- package/src/migrate/converter.ts +9 -1
- package/src/migrate/parser.ts +2 -0
- package/src/migrate/types.ts +2 -0
- package/src/service/snippet-validation.test.ts +1 -1
- package/src/service/snippet-validation.ts +2 -2
- package/src/setup.ts +69 -24
- package/src/viewer/__tests__/viewer-integration.test.ts +4 -10
- 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 +169 -2
- 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/DocsHeaderBar.tsx +6 -18
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
- 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 +114 -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/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-AWYCDRPG.js.map +0 -1
- package/dist/chunk-CJEGT3WD.js.map +0 -1
- package/dist/chunk-EKLMXTWU.js +0 -80
- package/dist/chunk-EKLMXTWU.js.map +0 -1
- package/dist/chunk-GOVI6COW.js +0 -195
- package/dist/chunk-GOVI6COW.js.map +0 -1
- package/dist/chunk-NGIMCIK2.js.map +0 -1
- package/dist/defineFragment-D0UTve-I.d.ts +0 -665
- package/dist/generate-35OIMW4Y.js +0 -252
- package/dist/generate-35OIMW4Y.js.map +0 -1
- package/dist/init-KSAAS7X3.js.map +0 -1
- package/dist/scan-65RH3QMM.js +0 -15
- package/dist/viewer-SBTJDMP7.js.map +0 -1
- package/src/core/__tests__/preview-runtime.test.tsx +0 -111
- package/src/core/composition.test.ts +0 -262
- package/src/core/composition.ts +0 -318
- package/src/core/constants.ts +0 -114
- package/src/core/context.ts +0 -2
- package/src/core/defineFragment.ts +0 -141
- package/src/core/figma.ts +0 -263
- package/src/core/fragment-types.ts +0 -214
- package/src/core/performance-presets.ts +0 -142
- package/src/core/preview-runtime.tsx +0 -144
- package/src/core/schema.ts +0 -221
- package/src/core/storyAdapter.test.ts +0 -571
- package/src/core/storyAdapter.ts +0 -761
- package/src/core/storybook-csf.ts +0 -11
- package/src/core/token-parser.ts +0 -321
- package/src/core/token-types.ts +0 -287
- package/src/core/types.ts +0 -762
- /package/dist/{chunk-WI6SLMSO.js.map → chunk-CAMXG5HJ.js.map} +0 -0
- /package/dist/{discovery-Z4RDDFVR.js.map → chunk-D2CDBRNU.js.map} +0 -0
- /package/dist/{chunk-YMPGYEWK.js.map → chunk-D5PYOXEI.js.map} +0 -0
- /package/dist/{chunk-TOIE7VXF.js.map → chunk-PW7QTQA6.js.map} +0 -0
- /package/dist/{scan-65RH3QMM.js.map → discovery-NEOY4MPN.js.map} +0 -0
- /package/dist/{service-A5GIGGGK.js.map → scan-OQU7M4GH.js.map} +0 -0
- /package/dist/{static-viewer-NSODM5VX.js.map → service-TQYWY65E.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;
|
|
@@ -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,
|
|
286
|
-
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
|
+
],
|
|
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,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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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>
|