@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
@@ -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;
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@fragments-sdk/ui';
4
+ import type { DocProp } from './types';
5
+ import styles from './PropsTable.module.scss';
6
+
7
+ /** Detect auto-generated descriptions like "variant prop" */
8
+ function isGenericPropDescription(name: string, description: string): boolean {
9
+ const lower = description.toLowerCase().trim();
10
+ return lower === `${name.toLowerCase()} prop` || lower === `${name.toLowerCase()} property`;
11
+ }
12
+
13
+ interface PropsTableProps {
14
+ props: Record<string, DocProp>;
15
+ }
16
+
17
+ export function PropsTable({ props }: PropsTableProps) {
18
+ const propEntries = Object.entries(props);
19
+
20
+ if (propEntries.length === 0) {
21
+ return <p className={styles.empty}>No props documented</p>;
22
+ }
23
+
24
+ return (
25
+ <div className={styles.container}>
26
+ <table className={styles.table}>
27
+ <thead>
28
+ <tr>
29
+ <th>Prop</th>
30
+ <th>Type</th>
31
+ <th>Default</th>
32
+ <th>Description</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ {propEntries.map(([name, prop]) => (
37
+ <tr key={name}>
38
+ <td>
39
+ <span className={styles.propName}>
40
+ <code>{name}</code>
41
+ {prop.required && (
42
+ <Badge size="sm" variant="error">
43
+ Required
44
+ </Badge>
45
+ )}
46
+ </span>
47
+ </td>
48
+ <td>
49
+ <span className={styles.type}>
50
+ <code>{prop.type}</code>
51
+ {prop.values && (
52
+ <span className={styles.values}>
53
+ {prop.values.map((v) => (
54
+ <code key={v} className={styles.value}>
55
+ {v}
56
+ </code>
57
+ ))}
58
+ </span>
59
+ )}
60
+ </span>
61
+ </td>
62
+ <td>
63
+ {prop.default !== undefined ? (
64
+ <code className={styles.default}>{String(prop.default)}</code>
65
+ ) : (
66
+ <span className={styles.noDefault}>Not set</span>
67
+ )}
68
+ </td>
69
+ <td>{prop.description && !isGenericPropDescription(name, prop.description) ? prop.description : '—'}</td>
70
+ </tr>
71
+ ))}
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,122 @@
1
+ // Shared variant preview card — used by docs and CLI viewer
2
+ .card {
3
+ border: 1px solid var(--fui-border);
4
+ border-radius: var(--fui-radius-lg);
5
+ overflow: hidden;
6
+ background-color: var(--fui-bg-primary);
7
+ }
8
+
9
+ .highlighted {
10
+ border-color: var(--fui-color-accent);
11
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--fui-color-accent) 40%, transparent);
12
+ }
13
+
14
+ .header {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ gap: var(--fui-space-4);
19
+ padding: var(--fui-space-2) var(--fui-space-4);
20
+ background-color: var(--fui-bg-primary);
21
+ border-bottom: 1px solid var(--fui-border);
22
+ }
23
+
24
+ .headerContent {
25
+ flex: 1;
26
+ min-width: 0;
27
+ }
28
+
29
+ .title {
30
+ font-size: var(--fui-font-size-sm);
31
+ font-weight: var(--fui-font-weight-semibold);
32
+ color: var(--fui-text-primary);
33
+ margin: 0;
34
+ line-height: 1.4;
35
+ }
36
+
37
+ .description {
38
+ font-size: var(--fui-font-size-xs);
39
+ color: var(--fui-text-tertiary);
40
+ margin: var(--fui-space-0-5) 0 0 0;
41
+ line-height: 1.4;
42
+ }
43
+
44
+ // Preview area with subtle dotted grid pattern
45
+ .previewPanel {
46
+ position: relative;
47
+ min-height: 400px;
48
+ padding: var(--fui-space-4);
49
+ display: flex;
50
+ flex-direction: column;
51
+ align-items: stretch;
52
+ background-color: var(--fui-bg-primary);
53
+
54
+ // Subtle dotted grid pattern
55
+ background-image: radial-gradient(circle, var(--fui-border) 1px, transparent 1px);
56
+ background-size: 24px 24px;
57
+
58
+ > * {
59
+ width: 100%;
60
+ flex: 1;
61
+ }
62
+ }
63
+
64
+ // Code panel
65
+ .codePanel {
66
+ background-color: var(--fui-bg-tertiary);
67
+ padding: 0 !important; // Override Tabs panel padding
68
+
69
+ // Override CodeBlock styles for embedded use
70
+ :global([class*="CodeBlock_container"]) {
71
+ border: none;
72
+ border-radius: 0;
73
+ margin: 0;
74
+ }
75
+
76
+ :global([class*="CodeBlock_wrapper"]) {
77
+ border-top-left-radius: 0;
78
+ border-top-right-radius: 0;
79
+ border: none;
80
+ }
81
+
82
+ :global([class*="CodeBlock_title"]) {
83
+ display: none;
84
+ }
85
+ }
86
+
87
+ .placeholder {
88
+ color: var(--fui-text-tertiary);
89
+ font-style: italic;
90
+ font-size: var(--fui-font-size-sm);
91
+ }
92
+
93
+ .headerActions {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: var(--fui-space-2);
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ // Override Tabs styling for preview cards
101
+ .card {
102
+ :global([class*="Tabs_root"]) {
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+
107
+ // Tabs list sits inline with header
108
+ :global([class*="Tabs_list"]) {
109
+ padding: 0;
110
+ border-bottom: none;
111
+ background-color: transparent;
112
+ flex-shrink: 0;
113
+ }
114
+
115
+ :global([class*="Tabs_listPills"]) {
116
+ margin: 0;
117
+ }
118
+
119
+ :global([class*="Tabs_panel"]) {
120
+ padding: 0;
121
+ }
122
+ }
@@ -0,0 +1,2 @@
1
+ declare const styles: Record<string, string>;
2
+ export default styles;
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { CodeBlock, Tabs } from '@fragments-sdk/ui';
5
+ import styles from './VariantPreviewCard.module.scss';
6
+
7
+ /** Detect auto-generated descriptions like "Alternative Layout variant" or "Default ActionMenu" */
8
+ function isGenericVariantDescription(name: string, description: string): boolean {
9
+ const lower = description.toLowerCase().trim();
10
+ const nameLower = name.toLowerCase().trim();
11
+ return (
12
+ lower === `${nameLower} variant` ||
13
+ lower === `default ${nameLower}` ||
14
+ lower === nameLower ||
15
+ // "Default SomeComponent" — "Default" followed by a PascalCase component name
16
+ /^default [a-z][a-z0-9]*$/i.test(description.trim())
17
+ );
18
+ }
19
+
20
+ export interface VariantPreviewCardProps {
21
+ name: string;
22
+ description?: string;
23
+ code?: string;
24
+ children: ReactNode;
25
+ isLoading?: boolean;
26
+ error?: string | null;
27
+ defaultTab?: 'preview' | 'code';
28
+ highlight?: boolean;
29
+ id?: string;
30
+ headerActions?: ReactNode;
31
+ className?: string;
32
+ }
33
+
34
+ export function VariantPreviewCard({
35
+ name,
36
+ description,
37
+ code,
38
+ children,
39
+ isLoading,
40
+ error,
41
+ defaultTab = 'preview',
42
+ highlight,
43
+ id,
44
+ headerActions,
45
+ className,
46
+ }: VariantPreviewCardProps) {
47
+ const cardClassName = [
48
+ styles.card,
49
+ highlight && styles.highlighted,
50
+ className,
51
+ ]
52
+ .filter(Boolean)
53
+ .join(' ');
54
+
55
+ const hasPreview = !!children;
56
+ const hasCode = typeof code === 'string' && code.length > 0;
57
+
58
+ // No preview, code only — render without tabs
59
+ if (!hasPreview && hasCode) {
60
+ return (
61
+ <div className={cardClassName} id={id}>
62
+ <div className={styles.header}>
63
+ <div className={styles.headerContent}>
64
+ <h3 className={styles.title}>{name}</h3>
65
+ {description && !isGenericVariantDescription(name, description) && <p className={styles.description}>{description}</p>}
66
+ </div>
67
+ {headerActions && <div className={styles.headerActions}>{headerActions}</div>}
68
+ </div>
69
+ <div className={styles.codePanel}>
70
+ <CodeBlock code={code} language="tsx" />
71
+ </div>
72
+ </div>
73
+ );
74
+ }
75
+
76
+ // No code — render preview only, no tabs
77
+ if (!hasCode) {
78
+ return (
79
+ <div className={cardClassName} id={id}>
80
+ <div className={styles.header}>
81
+ <div className={styles.headerContent}>
82
+ <h3 className={styles.title}>{name}</h3>
83
+ {description && !isGenericVariantDescription(name, description) && <p className={styles.description}>{description}</p>}
84
+ </div>
85
+ {headerActions && <div className={styles.headerActions}>{headerActions}</div>}
86
+ </div>
87
+ <div className={styles.previewPanel}>
88
+ {isLoading ? (
89
+ <div className={styles.placeholder}>Loading preview...</div>
90
+ ) : error ? (
91
+ <div className={styles.placeholder}>Preview error: {error}</div>
92
+ ) : (
93
+ children
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ // Both preview and code — render with tabs
101
+ return (
102
+ <div className={cardClassName} id={id}>
103
+ <Tabs defaultValue={defaultTab}>
104
+ <div className={styles.header}>
105
+ <div className={styles.headerContent}>
106
+ <h3 className={styles.title}>{name}</h3>
107
+ {description && !isGenericVariantDescription(name, description) && <p className={styles.description}>{description}</p>}
108
+ </div>
109
+ <div className={styles.headerActions}>
110
+ {headerActions}
111
+ <Tabs.List variant="pills">
112
+ <Tabs.Tab value="preview">Preview</Tabs.Tab>
113
+ <Tabs.Tab value="code">Code</Tabs.Tab>
114
+ </Tabs.List>
115
+ </div>
116
+ </div>
117
+
118
+ <Tabs.Panel value="preview" className={styles.previewPanel}>
119
+ {isLoading ? (
120
+ <div className={styles.placeholder}>Loading preview...</div>
121
+ ) : error ? (
122
+ <div className={styles.placeholder}>Preview error: {error}</div>
123
+ ) : (
124
+ children
125
+ )}
126
+ </Tabs.Panel>
127
+
128
+ <Tabs.Panel value="code" className={styles.codePanel}>
129
+ <CodeBlock code={code} language="tsx" />
130
+ </Tabs.Panel>
131
+ </Tabs>
132
+ </div>
133
+ );
134
+ }
@@ -24,3 +24,11 @@ export type {
24
24
  HeaderNavDropdown,
25
25
  } from './types';
26
26
  export { isDropdown } from './types';
27
+
28
+ export { VariantPreviewCard } from './VariantPreviewCard';
29
+ export type { VariantPreviewCardProps } from './VariantPreviewCard';
30
+
31
+ export { PropsTable } from './PropsTable';
32
+ export { ComponentDocContent } from './ComponentDocContent';
33
+ export type { ComponentDocContentProps } from './ComponentDocContent';
34
+ export type { DocProp } from './types';
@@ -39,3 +39,15 @@ export type HeaderNavEntry = HeaderNavLink | HeaderNavDropdown;
39
39
  export function isDropdown(entry: HeaderNavEntry): entry is HeaderNavDropdown {
40
40
  return 'items' in entry;
41
41
  }
42
+
43
+ /**
44
+ * Prop documentation type for shared PropsTable / ComponentDocContent.
45
+ * Intentionally simple — consumers map their richer types into this shape.
46
+ */
47
+ export interface DocProp {
48
+ type: string;
49
+ description: string;
50
+ required?: boolean;
51
+ default?: unknown;
52
+ values?: string[];
53
+ }
@@ -1506,7 +1506,7 @@ export function fragmentsPlugin(options: FragmentsPluginOptions): Plugin[] {
1506
1506
  // Load virtual modules
1507
1507
  load(id) {
1508
1508
  if (id === VIRTUAL_FRAGMENTS_RESOLVED) {
1509
- return generateFragmentsModule(fragmentFiles, config, previewConfigPath);
1509
+ return generateFragmentsModule(fragmentFiles, config, previewConfigPath, projectRoot);
1510
1510
  }
1511
1511
  if (id === VIRTUAL_VIEWER_ENTRY_RESOLVED) {
1512
1512
  return generateViewerEntry();
@@ -1636,6 +1636,19 @@ function extractComponentName(filePath: string): string {
1636
1636
  * Reads the file and looks for `dependencies: [{ name: '...' }, ...]` patterns.
1637
1637
  * Returns an array of package names, or empty array if extraction fails.
1638
1638
  */
1639
+ /**
1640
+ * Read the consumer project's package.json name field.
1641
+ * Used to show the correct import path in the docs view.
1642
+ */
1643
+ async function readProjectPackageName(root: string): Promise<string | null> {
1644
+ try {
1645
+ const pkgJson = JSON.parse(await readFile(resolve(root, "package.json"), "utf-8"));
1646
+ return pkgJson.name || null;
1647
+ } catch {
1648
+ return null;
1649
+ }
1650
+ }
1651
+
1639
1652
  async function extractDependenciesFromSource(absolutePath: string): Promise<string[]> {
1640
1653
  try {
1641
1654
  const source = await readFile(absolutePath, "utf-8");
@@ -1670,7 +1683,8 @@ async function extractDependenciesFromSource(absolutePath: string): Promise<stri
1670
1683
  async function generateFragmentsModule(
1671
1684
  fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
1672
1685
  config: FragmentsConfig,
1673
- previewConfigPath: string | null
1686
+ previewConfigPath: string | null,
1687
+ projectRoot: string
1674
1688
  ): Promise<string> {
1675
1689
  const authoredVariantCodeCache = new Map<string, Record<string, string>>();
1676
1690
 
@@ -1699,6 +1713,11 @@ async function generateFragmentsModule(
1699
1713
  }
1700
1714
  }
1701
1715
 
1716
+ // Compute sub-component map from story file paths (build-time)
1717
+ const storyOnlyFiles = fragmentFiles.filter(f => isStoryFile(f.relativePath));
1718
+ const { detectSubComponentPaths: _detectSubs } = await import("../core/storyFilters.js");
1719
+ const subComponentMap = _detectSubs(storyOnlyFiles);
1720
+
1702
1721
  // Group files by base component path to identify pairs
1703
1722
  const filesByBasePath = new Map<string, {
1704
1723
  storyFile?: { absolutePath: string; relativePath: string };
@@ -1750,11 +1769,16 @@ async function generateFragmentsModule(
1750
1769
  const fragmentSource = files.fragmentFile || primaryFile;
1751
1770
  const dependencies = await extractDependenciesFromSource(fragmentSource.absolutePath);
1752
1771
 
1772
+ // Sub-component detection (only applies to story files)
1773
+ const parentComponent = isStory ? subComponentMap.get(primaryFile.relativePath) : undefined;
1774
+
1753
1775
  return ` {
1754
1776
  path: "${primaryFile.relativePath}",
1755
1777
  isStory: ${isStory},
1756
1778
  componentName: ${JSON.stringify(componentName)},
1757
1779
  dependencies: ${JSON.stringify(dependencies)},
1780
+ isSubComponent: ${!!parentComponent},
1781
+ parentComponent: ${parentComponent ? JSON.stringify(parentComponent) : 'null'},
1758
1782
  loader: () => import("${primaryFile.absolutePath}"),
1759
1783
  metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : 'null'},
1760
1784
  authoredVariantCode: ${JSON.stringify(authoredVariantCode)}
@@ -1782,10 +1806,16 @@ setPreviewConfig({
1782
1806
  `
1783
1807
  : "";
1784
1808
 
1809
+ // Serialize storybook filter config for the virtual module
1810
+ const storybookFilterConfig = JSON.stringify(config.storybook ?? {});
1811
+
1785
1812
  return `
1786
- import { storyModuleToFragment, setPreviewConfig } from "@fragments-sdk/cli/core";
1813
+ import { storyModuleToFragment, setPreviewConfig, checkStoryExclusion, isForceIncluded, isConfigExcluded } from "@fragments-sdk/cli/core";
1787
1814
  ${previewImport}
1788
1815
  ${previewSetup}
1816
+ // Storybook filter config (deep-merged with defaults at build time)
1817
+ const storybookFilterConfig = ${storybookFilterConfig};
1818
+
1789
1819
  // Lazy fragment loaders (supports both .fragment.tsx and .stories.tsx)
1790
1820
  const fragmentLoaders = [
1791
1821
  ${loaders}
@@ -1848,8 +1878,24 @@ function mergeAuthoredVariantCode(fragment, authoredVariantCode) {
1848
1878
  return fragment;
1849
1879
  }
1850
1880
 
1881
+ // Diagnostics: track exclusions for FRAGMENTS_DEBUG
1882
+ const exclusionLog = [];
1883
+
1884
+ /**
1885
+ * Try to load a paired .fragment.tsx file as fallback when a story is excluded.
1886
+ * Returns the fragment if available, null otherwise.
1887
+ */
1888
+ async function tryFallbackFragment(loader) {
1889
+ if (!loader.metadataLoader) return null;
1890
+ try {
1891
+ const mod = await loader.metadataLoader();
1892
+ return mod?.default ? { path: loader.path, fragment: mod.default } : null;
1893
+ } catch { return null; }
1894
+ }
1895
+
1851
1896
  // Load all fragments (for initial render)
1852
1897
  // Gracefully handles individual failures - one bad story won't break all fragments
1898
+ // Applies smart filtering for Storybook stories (SVG icons, deprecated, test stories, sub-components)
1853
1899
  export async function loadAllFragments() {
1854
1900
  const results = await Promise.all(
1855
1901
  fragmentLoaders.map(async (loader) => {
@@ -1859,6 +1905,30 @@ export async function loadAllFragments() {
1859
1905
  return cached ? { path: loader.path, fragment: cached } : null;
1860
1906
  }
1861
1907
 
1908
+ // --- Pre-load filtering (config-level and sub-component checks) ---
1909
+ if (loader.isStory) {
1910
+ // Force-include bypasses all filters
1911
+ if (!isForceIncluded(loader.componentName, storybookFilterConfig)) {
1912
+ // Config explicit exclude
1913
+ if (isConfigExcluded(loader.componentName, storybookFilterConfig)) {
1914
+ exclusionLog.push({ component: loader.componentName, reason: 'config-excluded', detail: 'Matches storybook.exclude pattern', path: loader.path });
1915
+ const fallback = await tryFallbackFragment(loader);
1916
+ if (fallback) return fallback;
1917
+ loadedFragments.set(loader.path, null);
1918
+ return null;
1919
+ }
1920
+
1921
+ // Sub-component check (directory-based, computed at build time)
1922
+ if (loader.isSubComponent && storybookFilterConfig.excludeSubComponents !== false) {
1923
+ exclusionLog.push({ component: loader.componentName, reason: 'sub-component', detail: 'Sub-component of ' + loader.parentComponent, path: loader.path });
1924
+ const fallback = await tryFallbackFragment(loader);
1925
+ if (fallback) return fallback;
1926
+ loadedFragments.set(loader.path, null);
1927
+ return null;
1928
+ }
1929
+ }
1930
+ }
1931
+
1862
1932
  const module = await loader.loader();
1863
1933
 
1864
1934
  // Convert story modules to fragments at runtime
@@ -1870,6 +1940,29 @@ export async function loadAllFragments() {
1870
1940
  loadedFragments.set(loader.path, null);
1871
1941
  return null;
1872
1942
  }
1943
+
1944
+ // --- Post-load filtering (needs loaded module data) ---
1945
+ if (!isForceIncluded(loader.componentName, storybookFilterConfig)) {
1946
+ const meta = module.default || {};
1947
+ const exclusion = checkStoryExclusion({
1948
+ storybookTitle: meta.title,
1949
+ componentName: fragment.meta?.name || loader.componentName,
1950
+ componentDisplayName: meta.component?.displayName,
1951
+ componentFunctionName: meta.component?.name,
1952
+ tags: meta.tags,
1953
+ variantCount: fragment.variants?.length || 0,
1954
+ filePath: loader.path,
1955
+ config: storybookFilterConfig,
1956
+ });
1957
+
1958
+ if (exclusion.excluded) {
1959
+ exclusionLog.push({ component: fragment.meta?.name || loader.componentName, reason: exclusion.reason, detail: exclusion.detail, path: loader.path });
1960
+ const fallback = await tryFallbackFragment(loader);
1961
+ if (fallback) return fallback;
1962
+ loadedFragments.set(loader.path, null);
1963
+ return null;
1964
+ }
1965
+ }
1873
1966
  } else {
1874
1967
  fragment = module.default;
1875
1968
  }
@@ -1911,6 +2004,15 @@ export async function loadAllFragments() {
1911
2004
  }
1912
2005
  })
1913
2006
  );
2007
+
2008
+ // Log exclusions in debug mode
2009
+ if (exclusionLog.length > 0) {
2010
+ console.log("[Fragments] Filtered " + exclusionLog.length + " component(s)");
2011
+ if (typeof process !== 'undefined' && process.env?.FRAGMENTS_DEBUG) {
2012
+ console.table(exclusionLog);
2013
+ }
2014
+ }
2015
+
1914
2016
  // Filter out nulls (fragments that had no component)
1915
2017
  return results.filter(r => r !== null);
1916
2018
  }
@@ -1959,9 +2061,12 @@ export async function loadFragment(path) {
1959
2061
  let fragments = [];
1960
2062
  const fragmentsPromise = loadAllFragments().then(s => { fragments = s; return s; });
1961
2063
 
1962
- export { fragments, fragmentsPromise };
2064
+ export { fragments, fragmentsPromise, exclusionLog };
1963
2065
  export const config = ${JSON.stringify(config)};
1964
2066
 
2067
+ // Auto-detect consumer package name from package.json
2068
+ export const projectPackageName = ${JSON.stringify(await readProjectPackageName(projectRoot))};
2069
+
1965
2070
  // HMR support
1966
2071
  if (import.meta.hot) {
1967
2072
  import.meta.hot.accept();