@fragments-sdk/cli 0.7.10 → 0.7.12
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 +1 -1
- package/dist/{viewer-ZA7WK3EY.js → viewer-CNLZQUFO.js} +21 -4
- package/dist/viewer-CNLZQUFO.js.map +1 -0
- package/package.json +3 -2
- package/src/viewer/components/App.tsx +477 -126
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -1
- package/src/viewer/components/CodePanel.tsx +0 -1
- package/src/viewer/components/CommandPalette.tsx +17 -1
- package/src/viewer/components/IsolatedPreviewFrame.tsx +71 -74
- package/src/viewer/components/Layout.tsx +16 -13
- package/src/viewer/components/LeftSidebar.tsx +23 -3
- package/src/viewer/components/PreviewArea.tsx +21 -11
- package/src/viewer/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/viewer/server.ts +27 -3
- 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/dist/viewer-ZA7WK3EY.js.map +0 -1
- package/src/viewer/components/PreviewMenu.tsx +0 -247
|
@@ -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
|
+
}
|