@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.
@@ -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
+ }