@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.
- package/dist/bin.js +13 -13
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CWKQQR6C.js → chunk-57OW43NL.js} +3 -3
- package/dist/chunk-57OW43NL.js.map +1 -0
- package/dist/{chunk-AA6CAHCZ.js → chunk-7CRC46HV.js} +2 -2
- package/dist/chunk-7CRC46HV.js.map +1 -0
- package/dist/{chunk-3JPJTU25.js → chunk-CRTN6BIW.js} +5 -5
- package/dist/chunk-CRTN6BIW.js.map +1 -0
- package/dist/{chunk-LHIIBI6F.js → chunk-M42XIHPV.js} +2 -2
- package/dist/{chunk-2EFVPE5Q.js → chunk-TQOGBAOZ.js} +2 -2
- package/dist/chunk-TQOGBAOZ.js.map +1 -0
- package/dist/core/index.d.ts +1944 -0
- package/dist/{core-YAPWXDZW.js → core/index.js} +5 -5
- package/dist/defineFragment-C6PFzZyo.d.ts +656 -0
- package/dist/{generate-LEBVZCCH.js → generate-ZPERYZLF.js} +4 -4
- package/dist/index.d.ts +4 -159
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/{init-4VXL3Q6N.js → init-GID2DXB3.js} +69 -7
- package/dist/init-GID2DXB3.js.map +1 -0
- package/dist/mcp-bin.js +3 -3
- package/dist/{scan-3NYSRF6G.js → scan-BSMLGBX4.js} +5 -5
- package/dist/{service-HL6TMP3B.js → service-QACVPR37.js} +3 -3
- package/dist/{static-viewer-KLD24I4R.js → static-viewer-2RQD5QLR.js} +3 -3
- package/dist/{test-Y7YZOJLE.js → test-36UELXTE.js} +3 -3
- package/dist/{tokens-M4FCJKBK.js → tokens-A3BZIQPB.js} +4 -4
- package/dist/{viewer-ZWQQ74FV.js → viewer-CNLZQUFO.js} +156 -32
- package/dist/viewer-CNLZQUFO.js.map +1 -0
- package/package.json +8 -2
- package/src/commands/add.ts +1 -1
- package/src/commands/init.ts +84 -4
- package/src/core/defineFragment.ts +1 -1
- package/src/core/figma.ts +1 -1
- package/src/core/index.ts +2 -2
- package/src/core/loader.ts +3 -3
- package/src/core/schema.ts +1 -1
- package/src/index.ts +6 -0
- package/src/migrate/converter.ts +1 -1
- package/src/service/snippet-validation.test.ts +5 -5
- package/src/service/snippet-validation.ts +0 -1
- package/src/viewer/__tests__/viewer-integration.test.ts +16 -23
- package/src/viewer/components/AccessibilityPanel.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +1 -1
- package/src/viewer/components/App.tsx +563 -166
- package/src/viewer/components/BottomPanel.tsx +1 -1
- package/src/viewer/components/CodePanel.naming.test.tsx +1 -2
- package/src/viewer/components/CodePanel.tsx +1 -2
- package/src/viewer/components/CommandPalette.tsx +1 -1
- package/src/viewer/components/ComponentGraph.tsx +1 -1
- package/src/viewer/components/ComponentHeader.tsx +1 -1
- package/src/viewer/components/ContractPanel.tsx +1 -1
- package/src/viewer/components/ErrorBoundary.tsx +1 -1
- package/src/viewer/components/HealthDashboard.tsx +1 -1
- package/src/viewer/components/HmrStatusIndicator.tsx +1 -1
- package/src/viewer/components/InteractionsPanel.tsx +1 -1
- package/src/viewer/components/IsolatedRender.tsx +1 -1
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +1 -1
- package/src/viewer/components/LandingPage.tsx +1 -1
- package/src/viewer/components/Layout.tsx +16 -13
- package/src/viewer/components/LeftSidebar.tsx +105 -18
- package/src/viewer/components/MultiViewportPreview.tsx +1 -1
- package/src/viewer/components/PreviewArea.tsx +22 -13
- package/src/viewer/components/PreviewFrameHost.tsx +0 -4
- package/src/viewer/components/PreviewToolbar.tsx +1 -1
- package/src/viewer/components/PropsEditor.tsx +1 -1
- package/src/viewer/components/PropsTable.tsx +1 -1
- package/src/viewer/components/RightSidebar.tsx +1 -1
- package/src/viewer/components/ScreenshotButton.tsx +1 -1
- package/src/viewer/components/SkeletonLoader.tsx +1 -1
- package/src/viewer/components/Toast.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/VariantMatrix.tsx +1 -1
- package/src/viewer/components/VariantTabs.tsx +1 -1
- package/src/viewer/components/ViewportSelector.tsx +1 -1
- package/src/viewer/constants/ui.ts +14 -0
- package/src/viewer/entry.tsx +3 -4
- package/src/viewer/hooks/useKeyboardShortcuts.ts +65 -17
- package/src/viewer/hooks/useViewSettings.ts +1 -2
- package/src/viewer/index.ts +1 -1
- package/src/viewer/preview-frame.html +6 -9
- package/src/viewer/server.ts +106 -9
- package/src/viewer/styles/globals.css +12 -51
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +110 -0
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +89 -0
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +119 -0
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +134 -0
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +66 -0
- package/src/viewer/vendor/shared/src/docs-layout.scss +28 -0
- package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +2 -0
- package/src/viewer/vendor/shared/src/index.ts +26 -0
- package/src/viewer/vendor/shared/src/types.ts +41 -0
- package/src/viewer/vite-plugin.ts +70 -9
- package/dist/chunk-2EFVPE5Q.js.map +0 -1
- package/dist/chunk-3JPJTU25.js.map +0 -1
- package/dist/chunk-AA6CAHCZ.js.map +0 -1
- package/dist/chunk-CWKQQR6C.js.map +0 -1
- package/dist/init-4VXL3Q6N.js.map +0 -1
- package/dist/viewer-ZWQQ74FV.js.map +0 -1
- /package/dist/{chunk-LHIIBI6F.js.map → chunk-M42XIHPV.js.map} +0 -0
- /package/dist/{core-YAPWXDZW.js.map → core/index.js.map} +0 -0
- /package/dist/{generate-LEBVZCCH.js.map → generate-ZPERYZLF.js.map} +0 -0
- /package/dist/{scan-3NYSRF6G.js.map → scan-BSMLGBX4.js.map} +0 -0
- /package/dist/{service-HL6TMP3B.js.map → service-QACVPR37.js.map} +0 -0
- /package/dist/{static-viewer-KLD24I4R.js.map → static-viewer-2RQD5QLR.js.map} +0 -0
- /package/dist/{test-Y7YZOJLE.js.map → test-36UELXTE.js.map} +0 -0
- /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,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
|
|
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
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
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
|
|
1807
|
+
// Filter out nulls (fragments that had no component)
|
|
1747
1808
|
return results.filter(r => r !== null);
|
|
1748
1809
|
}
|
|
1749
1810
|
|