@fragments-sdk/cli 0.7.9 → 0.7.11

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 (106) hide show
  1. package/dist/bin.js +13 -13
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-CWKQQR6C.js → chunk-57OW43NL.js} +3 -3
  4. package/dist/chunk-57OW43NL.js.map +1 -0
  5. package/dist/{chunk-AA6CAHCZ.js → chunk-7CRC46HV.js} +2 -2
  6. package/dist/chunk-7CRC46HV.js.map +1 -0
  7. package/dist/{chunk-3JPJTU25.js → chunk-CRTN6BIW.js} +5 -5
  8. package/dist/chunk-CRTN6BIW.js.map +1 -0
  9. package/dist/{chunk-LHIIBI6F.js → chunk-M42XIHPV.js} +2 -2
  10. package/dist/{chunk-2EFVPE5Q.js → chunk-TQOGBAOZ.js} +2 -2
  11. package/dist/chunk-TQOGBAOZ.js.map +1 -0
  12. package/dist/core/index.d.ts +1944 -0
  13. package/dist/{core-YAPWXDZW.js → core/index.js} +5 -5
  14. package/dist/defineFragment-C6PFzZyo.d.ts +656 -0
  15. package/dist/{generate-LEBVZCCH.js → generate-ZPERYZLF.js} +4 -4
  16. package/dist/index.d.ts +4 -159
  17. package/dist/index.js +9 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-4VXL3Q6N.js → init-GID2DXB3.js} +69 -7
  20. package/dist/init-GID2DXB3.js.map +1 -0
  21. package/dist/mcp-bin.js +3 -3
  22. package/dist/{scan-3NYSRF6G.js → scan-BSMLGBX4.js} +5 -5
  23. package/dist/{service-HL6TMP3B.js → service-QACVPR37.js} +3 -3
  24. package/dist/{static-viewer-KLD24I4R.js → static-viewer-2RQD5QLR.js} +3 -3
  25. package/dist/{test-Y7YZOJLE.js → test-36UELXTE.js} +3 -3
  26. package/dist/{tokens-M4FCJKBK.js → tokens-A3BZIQPB.js} +4 -4
  27. package/dist/{viewer-ZWQQ74FV.js → viewer-CNLZQUFO.js} +156 -32
  28. package/dist/viewer-CNLZQUFO.js.map +1 -0
  29. package/package.json +8 -2
  30. package/src/commands/add.ts +1 -1
  31. package/src/commands/init.ts +84 -4
  32. package/src/core/defineFragment.ts +1 -1
  33. package/src/core/figma.ts +1 -1
  34. package/src/core/index.ts +2 -2
  35. package/src/core/loader.ts +3 -3
  36. package/src/core/schema.ts +1 -1
  37. package/src/index.ts +6 -0
  38. package/src/migrate/converter.ts +1 -1
  39. package/src/service/snippet-validation.test.ts +5 -5
  40. package/src/service/snippet-validation.ts +0 -1
  41. package/src/viewer/__tests__/viewer-integration.test.ts +16 -23
  42. package/src/viewer/components/AccessibilityPanel.tsx +1 -1
  43. package/src/viewer/components/ActionsPanel.tsx +1 -1
  44. package/src/viewer/components/App.tsx +563 -166
  45. package/src/viewer/components/BottomPanel.tsx +1 -1
  46. package/src/viewer/components/CodePanel.naming.test.tsx +1 -2
  47. package/src/viewer/components/CodePanel.tsx +1 -2
  48. package/src/viewer/components/CommandPalette.tsx +1 -1
  49. package/src/viewer/components/ComponentGraph.tsx +1 -1
  50. package/src/viewer/components/ComponentHeader.tsx +1 -1
  51. package/src/viewer/components/ContractPanel.tsx +1 -1
  52. package/src/viewer/components/ErrorBoundary.tsx +1 -1
  53. package/src/viewer/components/HealthDashboard.tsx +1 -1
  54. package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
  55. package/src/viewer/components/InteractionsPanel.tsx +1 -1
  56. package/src/viewer/components/IsolatedRender.tsx +1 -1
  57. package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
  58. package/src/viewer/components/LandingPage.tsx +1 -1
  59. package/src/viewer/components/Layout.tsx +16 -13
  60. package/src/viewer/components/LeftSidebar.tsx +105 -18
  61. package/src/viewer/components/MultiViewportPreview.tsx +1 -1
  62. package/src/viewer/components/PreviewArea.tsx +22 -13
  63. package/src/viewer/components/PreviewFrameHost.tsx +0 -4
  64. package/src/viewer/components/PreviewToolbar.tsx +1 -1
  65. package/src/viewer/components/PropsEditor.tsx +1 -1
  66. package/src/viewer/components/PropsTable.tsx +1 -1
  67. package/src/viewer/components/RightSidebar.tsx +1 -1
  68. package/src/viewer/components/ScreenshotButton.tsx +1 -1
  69. package/src/viewer/components/SkeletonLoader.tsx +1 -1
  70. package/src/viewer/components/Toast.tsx +2 -2
  71. package/src/viewer/components/TokenStylePanel.tsx +1 -1
  72. package/src/viewer/components/VariantMatrix.tsx +1 -1
  73. package/src/viewer/components/VariantTabs.tsx +1 -1
  74. package/src/viewer/components/ViewportSelector.tsx +1 -1
  75. package/src/viewer/constants/ui.ts +14 -0
  76. package/src/viewer/entry.tsx +3 -4
  77. package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
  78. package/src/viewer/hooks/useViewSettings.ts +1 -2
  79. package/src/viewer/index.ts +1 -1
  80. package/src/viewer/preview-frame.html +6 -9
  81. package/src/viewer/server.ts +106 -9
  82. package/src/viewer/styles/globals.css +12 -51
  83. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +110 -0
  84. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +89 -0
  85. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +119 -0
  86. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +134 -0
  87. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +66 -0
  88. package/src/viewer/vendor/shared/src/docs-layout.scss +28 -0
  89. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +2 -0
  90. package/src/viewer/vendor/shared/src/index.ts +26 -0
  91. package/src/viewer/vendor/shared/src/types.ts +41 -0
  92. package/src/viewer/vite-plugin.ts +70 -9
  93. package/dist/chunk-2EFVPE5Q.js.map +0 -1
  94. package/dist/chunk-3JPJTU25.js.map +0 -1
  95. package/dist/chunk-AA6CAHCZ.js.map +0 -1
  96. package/dist/chunk-CWKQQR6C.js.map +0 -1
  97. package/dist/init-4VXL3Q6N.js.map +0 -1
  98. package/dist/viewer-ZWQQ74FV.js.map +0 -1
  99. /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
  100. /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
  101. /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
  102. /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
  103. /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
  104. /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
  105. /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
  106. /package/dist/{tokens-M4FCJKBK.js.map → tokens-A3BZIQPB.js.map} +0 -0
@@ -0,0 +1,119 @@
1
+ 'use client';
2
+
3
+ import { AppShell } from '@fragments-sdk/ui';
4
+ import type { ReactNode } from 'react';
5
+ import { DocsPageAsideHost, DocsPageAsideProvider, useDocsPageAside } from './DocsPageAsideHost';
6
+
7
+ type SidebarCollapsible = 'offcanvas' | 'icon' | 'none';
8
+
9
+ interface DocsPageShellProps {
10
+ header: ReactNode;
11
+ sidebar: ReactNode;
12
+ children: ReactNode;
13
+ sidebarWidth?: string;
14
+ sidebarCollapsible?: SidebarCollapsible;
15
+ sidebarAriaLabel?: string;
16
+ mainId?: string;
17
+ mainAriaLabel?: string;
18
+ mainPadding?: 'none' | 'sm' | 'md' | 'lg';
19
+ mainClassName?: string;
20
+ aside?: ReactNode;
21
+ asideWidth?: string;
22
+ useAsidePortal?: boolean;
23
+ }
24
+
25
+ function DocsPageShellInner({
26
+ header,
27
+ sidebar,
28
+ children,
29
+ sidebarWidth = '260px',
30
+ sidebarCollapsible = 'offcanvas',
31
+ sidebarAriaLabel = 'Documentation sidebar',
32
+ mainId = 'main-content',
33
+ mainAriaLabel = 'Documentation content',
34
+ mainPadding = 'lg',
35
+ mainClassName,
36
+ aside,
37
+ asideWidth = '320px',
38
+ useAsidePortal = false,
39
+ }: DocsPageShellProps) {
40
+ return (
41
+ <AppShell>
42
+ <AppShell.Header>{header}</AppShell.Header>
43
+
44
+ <AppShell.Sidebar
45
+ width={sidebarWidth}
46
+ collapsible={sidebarCollapsible}
47
+ aria-label={sidebarAriaLabel}
48
+ >
49
+ {sidebar}
50
+ </AppShell.Sidebar>
51
+
52
+ <AppShell.Main
53
+ id={mainId}
54
+ aria-label={mainAriaLabel}
55
+ padding={mainPadding}
56
+ className={mainClassName}
57
+ >
58
+ {children}
59
+ </AppShell.Main>
60
+
61
+ {useAsidePortal ? <DocsPageAsideHost /> : null}
62
+ {!useAsidePortal && aside ? <AppShell.Aside width={asideWidth}>{aside}</AppShell.Aside> : null}
63
+ </AppShell>
64
+ );
65
+ }
66
+
67
+ function DocsPageShellPortalInner({
68
+ header,
69
+ sidebar,
70
+ children,
71
+ sidebarWidth = '260px',
72
+ sidebarCollapsible = 'offcanvas',
73
+ sidebarAriaLabel = 'Documentation sidebar',
74
+ mainId = 'main-content',
75
+ mainAriaLabel = 'Documentation content',
76
+ mainPadding = 'lg',
77
+ mainClassName,
78
+ }: DocsPageShellProps) {
79
+ const { asideVisible, asideWidth } = useDocsPageAside();
80
+
81
+ return (
82
+ <AppShell>
83
+ <AppShell.Header>{header}</AppShell.Header>
84
+
85
+ <AppShell.Sidebar
86
+ width={sidebarWidth}
87
+ collapsible={sidebarCollapsible}
88
+ aria-label={sidebarAriaLabel}
89
+ >
90
+ {sidebar}
91
+ </AppShell.Sidebar>
92
+
93
+ <AppShell.Main
94
+ id={mainId}
95
+ aria-label={mainAriaLabel}
96
+ padding={mainPadding}
97
+ className={mainClassName}
98
+ >
99
+ {children}
100
+ </AppShell.Main>
101
+
102
+ <AppShell.Aside width={asideWidth} visible={asideVisible} aria-label="On-page controls">
103
+ <DocsPageAsideHost />
104
+ </AppShell.Aside>
105
+ </AppShell>
106
+ );
107
+ }
108
+
109
+ export function DocsPageShell(props: DocsPageShellProps) {
110
+ if (props.useAsidePortal) {
111
+ return (
112
+ <DocsPageAsideProvider defaultWidth={props.asideWidth}>
113
+ <DocsPageShellPortalInner {...props} />
114
+ </DocsPageAsideProvider>
115
+ );
116
+ }
117
+
118
+ return <DocsPageShellInner {...props} />;
119
+ }
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import { Input, Listbox } from '@fragments-sdk/ui';
4
+ import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
5
+ import type { SearchItem } from './types';
6
+
7
+ interface DocsSearchCommandProps {
8
+ searchItems: SearchItem[];
9
+ onSelect: (item: SearchItem) => void;
10
+ placeholder?: string;
11
+ maxResults?: number;
12
+ }
13
+
14
+ export function DocsSearchCommand({
15
+ searchItems,
16
+ onSelect,
17
+ placeholder = 'Search...',
18
+ maxResults = 8,
19
+ }: DocsSearchCommandProps) {
20
+ const [query, setQuery] = useState('');
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const [selectedIndex, setSelectedIndex] = useState(0);
23
+ const inputRef = useRef<HTMLInputElement>(null);
24
+ const containerRef = useRef<HTMLDivElement>(null);
25
+
26
+ const results = useMemo(() => {
27
+ if (!query.trim()) return [];
28
+ const lowerQuery = query.toLowerCase();
29
+ return searchItems
30
+ .filter((item) => item.label.toLowerCase().includes(lowerQuery) || item.section.toLowerCase().includes(lowerQuery))
31
+ .slice(0, maxResults);
32
+ }, [maxResults, query, searchItems]);
33
+
34
+ useEffect(() => {
35
+ setSelectedIndex(0);
36
+ }, [results]);
37
+
38
+ useEffect(() => {
39
+ const handleGlobalKeyDown = (event: KeyboardEvent) => {
40
+ if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
41
+ event.preventDefault();
42
+ inputRef.current?.focus();
43
+ setIsOpen(true);
44
+ }
45
+ };
46
+
47
+ document.addEventListener('keydown', handleGlobalKeyDown);
48
+ return () => document.removeEventListener('keydown', handleGlobalKeyDown);
49
+ }, []);
50
+
51
+ useEffect(() => {
52
+ const handleClickOutside = (event: MouseEvent) => {
53
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
54
+ setIsOpen(false);
55
+ }
56
+ };
57
+
58
+ document.addEventListener('mousedown', handleClickOutside);
59
+ return () => document.removeEventListener('mousedown', handleClickOutside);
60
+ }, []);
61
+
62
+ const handleSelect = (item: SearchItem) => {
63
+ onSelect(item);
64
+ setIsOpen(false);
65
+ setQuery('');
66
+ };
67
+
68
+ const handleKeyDown = (event: ReactKeyboardEvent) => {
69
+ if (!isOpen || results.length === 0) return;
70
+
71
+ switch (event.key) {
72
+ case 'ArrowDown':
73
+ event.preventDefault();
74
+ setSelectedIndex((prev) => (prev + 1) % results.length);
75
+ break;
76
+ case 'ArrowUp':
77
+ event.preventDefault();
78
+ setSelectedIndex((prev) => (prev - 1 + results.length) % results.length);
79
+ break;
80
+ case 'Enter':
81
+ event.preventDefault();
82
+ if (results[selectedIndex]) {
83
+ handleSelect(results[selectedIndex]);
84
+ inputRef.current?.blur();
85
+ }
86
+ break;
87
+ case 'Escape':
88
+ setIsOpen(false);
89
+ inputRef.current?.blur();
90
+ break;
91
+ default:
92
+ break;
93
+ }
94
+ };
95
+
96
+ return (
97
+ <div ref={containerRef} className="shared-docs-search-container">
98
+ <Input
99
+ ref={inputRef}
100
+ placeholder={placeholder}
101
+ aria-label="Search"
102
+ value={query}
103
+ onChange={(value) => {
104
+ setQuery(value);
105
+ setIsOpen(true);
106
+ }}
107
+ onFocus={() => setIsOpen(true)}
108
+ onKeyDown={handleKeyDown}
109
+ shortcut="⌘K"
110
+ size="sm"
111
+ />
112
+ {isOpen && results.length > 0 && (
113
+ <Listbox aria-label="Search results" className="shared-docs-search-results">
114
+ {results.map((item, index) => (
115
+ <Listbox.Item
116
+ key={`${item.section}:${item.href}`}
117
+ selected={index === selectedIndex}
118
+ onClick={() => handleSelect(item)}
119
+ onMouseEnter={() => setSelectedIndex(index)}
120
+ >
121
+ <span className="shared-docs-search-result-label">{item.label}</span>
122
+ <span className="shared-docs-search-result-section">{item.section}</span>
123
+ </Listbox.Item>
124
+ ))}
125
+ </Listbox>
126
+ )}
127
+ {isOpen && query.trim() && results.length === 0 && (
128
+ <Listbox aria-label="Search results" className="shared-docs-search-results">
129
+ <Listbox.Empty>No results found</Listbox.Empty>
130
+ </Listbox>
131
+ )}
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { Sidebar } from '@fragments-sdk/ui';
4
+ import type { ReactNode } from 'react';
5
+ import type { DocsNavLinkRenderer, NavSection } from './types';
6
+
7
+ interface DocsSidebarNavProps {
8
+ sections: NavSection[];
9
+ currentPath: string;
10
+ renderLink?: DocsNavLinkRenderer;
11
+ onNavigate?: () => void;
12
+ isActive?: (href: string, currentPath: string) => boolean;
13
+ footer?: ReactNode;
14
+ ariaLabel?: string;
15
+ }
16
+
17
+ function defaultIsActive(href: string, currentPath: string): boolean {
18
+ return currentPath === href;
19
+ }
20
+
21
+ const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
22
+ <a href={href} onClick={onClick}>
23
+ {label}
24
+ </a>
25
+ );
26
+
27
+ export function DocsSidebarNav({
28
+ sections,
29
+ currentPath,
30
+ renderLink = defaultLinkRenderer,
31
+ onNavigate,
32
+ isActive = defaultIsActive,
33
+ footer,
34
+ ariaLabel = 'Navigation',
35
+ }: DocsSidebarNavProps) {
36
+ return (
37
+ <>
38
+ <Sidebar.Nav aria-label={ariaLabel}>
39
+ {sections.map((section) => (
40
+ <Sidebar.Section
41
+ key={section.title}
42
+ label={section.title}
43
+ collapsible={section.collapsible}
44
+ defaultOpen={section.defaultOpen}
45
+ >
46
+ {section.items.map((item) => (
47
+ <Sidebar.Item
48
+ key={item.href}
49
+ active={isActive(item.href, currentPath)}
50
+ asChild
51
+ >
52
+ {renderLink({
53
+ href: item.href,
54
+ label: item.label,
55
+ onClick: onNavigate,
56
+ })}
57
+ </Sidebar.Item>
58
+ ))}
59
+ </Sidebar.Section>
60
+ ))}
61
+ </Sidebar.Nav>
62
+
63
+ <Sidebar.Footer>{footer}</Sidebar.Footer>
64
+ </>
65
+ );
66
+ }
@@ -0,0 +1,28 @@
1
+ .shared-docs-main-content {
2
+ width: 100%;
3
+ }
4
+
5
+ .shared-docs-search-container {
6
+ position: relative;
7
+ width: 100%;
8
+ max-width: 240px;
9
+ }
10
+
11
+ .shared-docs-search-results {
12
+ position: absolute;
13
+ top: calc(100% + 4px);
14
+ left: 0;
15
+ right: 0;
16
+ z-index: 100;
17
+ }
18
+
19
+ .shared-docs-search-result-label {
20
+ flex: 1;
21
+ font-weight: var(--fui-font-weight-medium);
22
+ }
23
+
24
+ .shared-docs-search-result-section {
25
+ font-size: var(--fui-font-size-xs);
26
+ color: var(--fui-text-tertiary);
27
+ margin-left: auto;
28
+ }
@@ -0,0 +1,2 @@
1
+ declare const styles: string;
2
+ export default styles;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import './docs-layout.scss';
4
+
5
+ export { DocsPageShell } from './DocsPageShell';
6
+ export { DocsSidebarNav } from './DocsSidebarNav';
7
+ export { DocsSearchCommand } from './DocsSearchCommand';
8
+ export { DocsHeaderBar } from './DocsHeaderBar';
9
+ export {
10
+ DocsPageAsideHost,
11
+ DocsPageAsidePortal,
12
+ DocsPageAsideProvider,
13
+ useDocsPageAside,
14
+ } from './DocsPageAsideHost';
15
+
16
+ export type {
17
+ NavItem,
18
+ NavSection,
19
+ SearchItem,
20
+ DocsNavLinkRenderProps,
21
+ DocsNavLinkRenderer,
22
+ HeaderNavEntry,
23
+ HeaderNavLink,
24
+ HeaderNavDropdown,
25
+ } from './types';
26
+ export { isDropdown } from './types';
@@ -0,0 +1,41 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ export interface NavItem {
4
+ label: string;
5
+ href: string;
6
+ }
7
+
8
+ export interface NavSection {
9
+ title: string;
10
+ items: NavItem[];
11
+ collapsible?: boolean;
12
+ defaultOpen?: boolean;
13
+ }
14
+
15
+ export interface SearchItem extends NavItem {
16
+ section: string;
17
+ }
18
+
19
+ export interface DocsNavLinkRenderProps {
20
+ href: string;
21
+ label: string;
22
+ onClick?: () => void;
23
+ }
24
+
25
+ export type DocsNavLinkRenderer = (props: DocsNavLinkRenderProps) => ReactElement;
26
+
27
+ export interface HeaderNavLink {
28
+ label: string;
29
+ href: string;
30
+ }
31
+
32
+ export interface HeaderNavDropdown {
33
+ label: string;
34
+ items: NavItem[];
35
+ }
36
+
37
+ export type HeaderNavEntry = HeaderNavLink | HeaderNavDropdown;
38
+
39
+ export function isDropdown(entry: HeaderNavEntry): entry is HeaderNavDropdown {
40
+ return 'items' in entry;
41
+ }
@@ -1566,6 +1566,41 @@ function getBaseComponentPath(filePath: string): string {
1566
1566
  return filePath.replace(/\.(fragment|stories)\.(tsx?|jsx?)$/, "");
1567
1567
  }
1568
1568
 
1569
+ /**
1570
+ * Extract component name from a fragment/story file path.
1571
+ * e.g., "src/components/Chart/Chart.fragment.tsx" -> "Chart"
1572
+ * e.g., "@fragments-sdk/ui/src/components/Button/Button.fragment.tsx" -> "Button"
1573
+ */
1574
+ function extractComponentName(filePath: string): string {
1575
+ const match = filePath.match(/([^/\\]+)\.(fragment|stories)\.(tsx?|jsx?)$/);
1576
+ return match ? match[1] : filePath.split("/").pop() || filePath;
1577
+ }
1578
+
1579
+ /**
1580
+ * Extract dependency names from a fragment source file (best effort).
1581
+ * Reads the file and looks for `dependencies: [{ name: '...' }, ...]` patterns.
1582
+ * Returns an array of package names, or empty array if extraction fails.
1583
+ */
1584
+ async function extractDependenciesFromSource(absolutePath: string): Promise<string[]> {
1585
+ try {
1586
+ const source = await readFile(absolutePath, "utf-8");
1587
+ // Match the dependencies array in the defineFragment call
1588
+ const depsMatch = source.match(/dependencies:\s*\[([\s\S]*?)\]/);
1589
+ if (!depsMatch) return [];
1590
+
1591
+ // Extract name values from dependency objects
1592
+ const names: string[] = [];
1593
+ const nameRegex = /name:\s*['"]([^'"]+)['"]/g;
1594
+ let match;
1595
+ while ((match = nameRegex.exec(depsMatch[1])) !== null) {
1596
+ names.push(match[1]);
1597
+ }
1598
+ return names;
1599
+ } catch {
1600
+ return [];
1601
+ }
1602
+ }
1603
+
1569
1604
  /**
1570
1605
  * Generate the virtual fragments module.
1571
1606
  * Uses dynamic imports for lazy loading - fragments are loaded on demand.
@@ -1577,11 +1612,11 @@ function getBaseComponentPath(filePath: string): string {
1577
1612
  * - Merge METADATA from .fragment.tsx (Figma URLs, AI descriptions, usage guidelines)
1578
1613
  * - This gives us the best of both worlds: working renders + rich metadata
1579
1614
  */
1580
- function generateFragmentsModule(
1615
+ async function generateFragmentsModule(
1581
1616
  fragmentFiles: Array<{ absolutePath: string; relativePath: string }>,
1582
1617
  config: FragmentsConfig,
1583
1618
  previewConfigPath: string | null
1584
- ): string {
1619
+ ): Promise<string> {
1585
1620
  // Group files by base component path to identify pairs
1586
1621
  const filesByBasePath = new Map<string, {
1587
1622
  storyFile?: { absolutePath: string; relativePath: string };
@@ -1605,8 +1640,8 @@ function generateFragmentsModule(
1605
1640
 
1606
1641
  // Generate loaders with metadata merge support
1607
1642
  // Priority: stories for rendering, fragment for metadata (Figma URLs, etc.)
1608
- const loaders = Array.from(filesByBasePath.values())
1609
- .map((files) => {
1643
+ const loaderEntries = await Promise.all(
1644
+ Array.from(filesByBasePath.values()).map(async (files) => {
1610
1645
  // Determine which file to use for rendering
1611
1646
  const primaryFile = files.storyFile || files.fragmentFile;
1612
1647
  if (!primaryFile) return null;
@@ -1618,15 +1653,25 @@ function generateFragmentsModule(
1618
1653
  ? files.fragmentFile.absolutePath
1619
1654
  : null;
1620
1655
 
1656
+ // Extract component name from file path
1657
+ const componentName = extractComponentName(primaryFile.relativePath);
1658
+
1659
+ // Extract dependencies from source (best effort, for error stubs)
1660
+ const fragmentSource = files.fragmentFile || primaryFile;
1661
+ const dependencies = await extractDependenciesFromSource(fragmentSource.absolutePath);
1662
+
1621
1663
  return ` {
1622
1664
  path: "${primaryFile.relativePath}",
1623
1665
  isStory: ${isStory},
1666
+ componentName: ${JSON.stringify(componentName)},
1667
+ dependencies: ${JSON.stringify(dependencies)},
1624
1668
  loader: () => import("${primaryFile.absolutePath}"),
1625
1669
  metadataLoader: ${metadataPath ? `() => import("${metadataPath}")` : 'null'}
1626
1670
  }`;
1627
1671
  })
1628
- .filter(Boolean)
1629
- .join(",\n");
1672
+ );
1673
+
1674
+ const loaders = loaderEntries.filter(Boolean).join(",\n");
1630
1675
 
1631
1676
  // Generate preview config import if available
1632
1677
  const previewImport = previewConfigPath
@@ -1647,7 +1692,7 @@ setPreviewConfig({
1647
1692
  : "";
1648
1693
 
1649
1694
  return `
1650
- import { storyModuleToFragment, setPreviewConfig } from "@fragments/core";
1695
+ import { storyModuleToFragment, setPreviewConfig } from "@fragments-sdk/cli/core";
1651
1696
  ${previewImport}
1652
1697
  ${previewSetup}
1653
1698
  // Lazy fragment loaders (supports both .fragment.tsx and .stories.tsx)
@@ -1739,11 +1784,27 @@ export async function loadAllFragments() {
1739
1784
  return { path: loader.path, fragment };
1740
1785
  } catch (error) {
1741
1786
  console.warn("[Fragments] Failed to load " + loader.path + ":", error.message);
1742
- return null;
1787
+ // Create an error stub so the component still appears in the sidebar
1788
+ // with a helpful message about what went wrong
1789
+ return {
1790
+ path: loader.path,
1791
+ fragment: {
1792
+ meta: {
1793
+ name: loader.componentName || loader.path,
1794
+ description: 'Failed to load',
1795
+ category: '',
1796
+ },
1797
+ variants: [],
1798
+ _loadError: {
1799
+ message: error.message,
1800
+ dependencies: loader.dependencies || [],
1801
+ },
1802
+ },
1803
+ };
1743
1804
  }
1744
1805
  })
1745
1806
  );
1746
- // Filter out failed loads
1807
+ // Filter out nulls (fragments that had no component)
1747
1808
  return results.filter(r => r !== null);
1748
1809
  }
1749
1810