@fragments-sdk/viewer 0.2.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.
- package/LICENSE +84 -0
- package/index.html +28 -0
- package/package.json +71 -0
- package/src/__tests__/a11y-fixes.test.ts +358 -0
- package/src/__tests__/jsx-parser.test.ts +502 -0
- package/src/__tests__/render-utils.test.ts +232 -0
- package/src/__tests__/style-utils.test.ts +404 -0
- package/src/app/index.ts +1 -0
- package/src/assets/fragments-logo.ts +4 -0
- package/src/assets/fragments_logo.png +0 -0
- package/src/components/AccessibilityPanel.tsx +1457 -0
- package/src/components/ActionCapture.tsx +172 -0
- package/src/components/ActionsPanel.tsx +332 -0
- package/src/components/AllVariantsPreview.tsx +78 -0
- package/src/components/App.tsx +604 -0
- package/src/components/BottomPanel.tsx +288 -0
- package/src/components/CodePanel.naming.test.tsx +59 -0
- package/src/components/CodePanel.tsx +118 -0
- package/src/components/CommandPalette.tsx +392 -0
- package/src/components/ComponentDocView.tsx +164 -0
- package/src/components/ComponentGraph.tsx +380 -0
- package/src/components/ComponentHeader.tsx +88 -0
- package/src/components/ContractPanel.tsx +241 -0
- package/src/components/DeviceMockup.tsx +156 -0
- package/src/components/EmptyVariantMessage.tsx +54 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/FigmaEmbed.tsx +238 -0
- package/src/components/FragmentEditor.tsx +525 -0
- package/src/components/FragmentRenderer.tsx +61 -0
- package/src/components/HeaderSearch.tsx +24 -0
- package/src/components/HealthDashboard.tsx +441 -0
- package/src/components/HmrStatusIndicator.tsx +61 -0
- package/src/components/Icons.tsx +479 -0
- package/src/components/InteractionsPanel.tsx +757 -0
- package/src/components/IsolatedPreviewFrame.tsx +390 -0
- package/src/components/IsolatedRender.tsx +113 -0
- package/src/components/KeyboardShortcutsHelp.tsx +53 -0
- package/src/components/LandingPage.tsx +420 -0
- package/src/components/Layout.tsx +27 -0
- package/src/components/LeftSidebar.tsx +472 -0
- package/src/components/LoadErrorMessage.tsx +102 -0
- package/src/components/MultiViewportPreview.tsx +527 -0
- package/src/components/NoVariantsMessage.tsx +59 -0
- package/src/components/PanelShell.tsx +161 -0
- package/src/components/PerformancePanel.tsx +304 -0
- package/src/components/PreviewArea.tsx +254 -0
- package/src/components/PreviewAside.tsx +168 -0
- package/src/components/PreviewFrameHost.tsx +304 -0
- package/src/components/PreviewToolbar.tsx +80 -0
- package/src/components/PropsEditor.tsx +506 -0
- package/src/components/PropsTable.tsx +111 -0
- package/src/components/RelationsSection.tsx +88 -0
- package/src/components/ResizablePanel.tsx +271 -0
- package/src/components/RightSidebar.tsx +102 -0
- package/src/components/RuntimeToolsRegistrar.tsx +17 -0
- package/src/components/ScreenshotButton.tsx +90 -0
- package/src/components/ShadowPreview.tsx +204 -0
- package/src/components/Sidebar.tsx +169 -0
- package/src/components/SkeletonLoader.tsx +161 -0
- package/src/components/ThemeProvider.tsx +42 -0
- package/src/components/Toast.tsx +3 -0
- package/src/components/TokenStylePanel.tsx +699 -0
- package/src/components/TopToolbar.tsx +159 -0
- package/src/components/Untitled +1 -0
- package/src/components/UsageSection.tsx +95 -0
- package/src/components/VariantMatrix.tsx +391 -0
- package/src/components/VariantRenderer.tsx +131 -0
- package/src/components/VariantTabs.tsx +40 -0
- package/src/components/ViewerHeader.tsx +69 -0
- package/src/components/ViewerStateSync.tsx +52 -0
- package/src/components/ViewportSelector.tsx +172 -0
- package/src/components/WebMCPDevTools.tsx +503 -0
- package/src/components/WebMCPIntegration.tsx +47 -0
- package/src/components/WebMCPStatusIndicator.tsx +60 -0
- package/src/components/_future/CreatePage.tsx +835 -0
- package/src/components/viewer-utils.ts +16 -0
- package/src/composition-renderer.ts +381 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/ui.ts +166 -0
- package/src/entry.tsx +335 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useA11yCache.ts +383 -0
- package/src/hooks/useA11yService.ts +364 -0
- package/src/hooks/useActions.ts +138 -0
- package/src/hooks/useAppState.ts +147 -0
- package/src/hooks/useCompiledFragments.ts +42 -0
- package/src/hooks/useFigmaIntegration.ts +132 -0
- package/src/hooks/useHmrStatus.ts +109 -0
- package/src/hooks/useKeyboardShortcuts.ts +270 -0
- package/src/hooks/usePreviewBridge.ts +347 -0
- package/src/hooks/useScrollSpy.ts +78 -0
- package/src/hooks/useShadowStyles.ts +221 -0
- package/src/hooks/useUrlState.ts +318 -0
- package/src/hooks/useViewSettings.ts +111 -0
- package/src/intelligence/healthReport.ts +505 -0
- package/src/intelligence/styleDrift.ts +340 -0
- package/src/intelligence/usageScanner.ts +309 -0
- package/src/jsx-parser.ts +486 -0
- package/src/preview-frame-entry.tsx +25 -0
- package/src/preview-frame.html +148 -0
- package/src/render-template.html +68 -0
- package/src/render-utils.ts +311 -0
- package/src/shared/ComponentDocContent.module.scss +10 -0
- package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
- package/src/shared/ComponentDocContent.tsx +274 -0
- package/src/shared/DocsHeaderBar.tsx +129 -0
- package/src/shared/DocsPageAsideHost.tsx +89 -0
- package/src/shared/DocsPageShell.tsx +124 -0
- package/src/shared/DocsSearchCommand.tsx +99 -0
- package/src/shared/DocsSidebarNav.tsx +66 -0
- package/src/shared/PropsTable.module.scss +68 -0
- package/src/shared/PropsTable.module.scss.d.ts +2 -0
- package/src/shared/PropsTable.tsx +76 -0
- package/src/shared/VariantPreviewCard.module.scss +114 -0
- package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
- package/src/shared/VariantPreviewCard.tsx +137 -0
- package/src/shared/docs-data/index.ts +32 -0
- package/src/shared/docs-data/mcp-configs.ts +72 -0
- package/src/shared/docs-data/palettes.ts +75 -0
- package/src/shared/docs-data/setup-examples.ts +55 -0
- package/src/shared/docs-layout.scss +28 -0
- package/src/shared/docs-layout.scss.d.ts +2 -0
- package/src/shared/index.ts +34 -0
- package/src/shared/types.ts +53 -0
- package/src/style-utils.ts +414 -0
- package/src/styles/globals.css +278 -0
- package/src/types/a11y.ts +197 -0
- package/src/utils/a11y-fixes.ts +509 -0
- package/src/utils/actionExport.ts +372 -0
- package/src/utils/colorSchemes.ts +201 -0
- package/src/utils/contrast.ts +246 -0
- package/src/utils/detectRelationships.ts +256 -0
- package/src/webmcp/__tests__/analytics.test.ts +108 -0
- package/src/webmcp/analytics.ts +165 -0
- package/src/webmcp/index.ts +3 -0
- package/src/webmcp/posthog-bridge.ts +39 -0
- package/src/webmcp/runtime-tools.ts +152 -0
- package/src/webmcp/scan-utils.ts +135 -0
- package/src/webmcp/use-tool-analytics.ts +69 -0
- package/src/webmcp/viewer-state.ts +45 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Header, NavigationMenu } from '@fragments-sdk/ui';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { DocsSearchCommand } from './DocsSearchCommand';
|
|
6
|
+
import type { DocsNavLinkRenderer, HeaderNavEntry, NavSection, SearchItem } from './types';
|
|
7
|
+
import { isDropdown } from './types';
|
|
8
|
+
|
|
9
|
+
interface DocsHeaderBarProps {
|
|
10
|
+
brand: ReactNode;
|
|
11
|
+
headerNav: HeaderNavEntry[];
|
|
12
|
+
mobileSections?: NavSection[];
|
|
13
|
+
currentPath: string;
|
|
14
|
+
searchItems: SearchItem[];
|
|
15
|
+
onSearchSelect: (item: SearchItem) => void;
|
|
16
|
+
renderLink?: DocsNavLinkRenderer;
|
|
17
|
+
isActive?: (href: string, currentPath: string) => boolean;
|
|
18
|
+
actions?: ReactNode;
|
|
19
|
+
showSkipLink?: boolean;
|
|
20
|
+
navAriaLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
|
|
24
|
+
<a href={href} onClick={onClick}>
|
|
25
|
+
{label}
|
|
26
|
+
</a>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function defaultIsActive(href: string, currentPath: string): boolean {
|
|
30
|
+
return currentPath === href || currentPath.startsWith(`${href}/`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function DocsHeaderBar({
|
|
34
|
+
brand,
|
|
35
|
+
headerNav,
|
|
36
|
+
mobileSections = [],
|
|
37
|
+
currentPath,
|
|
38
|
+
searchItems,
|
|
39
|
+
onSearchSelect,
|
|
40
|
+
renderLink = defaultLinkRenderer,
|
|
41
|
+
isActive = defaultIsActive,
|
|
42
|
+
actions,
|
|
43
|
+
showSkipLink = true,
|
|
44
|
+
navAriaLabel = 'Primary navigation',
|
|
45
|
+
}: DocsHeaderBarProps) {
|
|
46
|
+
return (
|
|
47
|
+
<Header aria-label="Documentation header">
|
|
48
|
+
<Header.Brand>{brand}</Header.Brand>
|
|
49
|
+
{showSkipLink ? <Header.SkipLink /> : null}
|
|
50
|
+
|
|
51
|
+
<NavigationMenu aria-label={navAriaLabel}>
|
|
52
|
+
<NavigationMenu.List>
|
|
53
|
+
{headerNav.map((entry) =>
|
|
54
|
+
isDropdown(entry) ? (
|
|
55
|
+
<NavigationMenu.Item key={entry.label} value={entry.label}>
|
|
56
|
+
<NavigationMenu.Trigger>{entry.label}</NavigationMenu.Trigger>
|
|
57
|
+
<NavigationMenu.Content>
|
|
58
|
+
<div style={{ display: 'flex', flexDirection: 'column', padding: '4px', minWidth: '180px' }}>
|
|
59
|
+
{entry.items.map((child) => (
|
|
60
|
+
<NavigationMenu.Link
|
|
61
|
+
key={child.href}
|
|
62
|
+
href={child.href}
|
|
63
|
+
active={isActive(child.href, currentPath)}
|
|
64
|
+
asChild
|
|
65
|
+
>
|
|
66
|
+
{renderLink({ href: child.href, label: child.label })}
|
|
67
|
+
</NavigationMenu.Link>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</NavigationMenu.Content>
|
|
71
|
+
</NavigationMenu.Item>
|
|
72
|
+
) : (
|
|
73
|
+
<NavigationMenu.Item key={entry.href}>
|
|
74
|
+
<NavigationMenu.Link
|
|
75
|
+
href={entry.href}
|
|
76
|
+
active={isActive(entry.href, currentPath)}
|
|
77
|
+
asChild
|
|
78
|
+
>
|
|
79
|
+
{renderLink({ href: entry.href, label: entry.label })}
|
|
80
|
+
</NavigationMenu.Link>
|
|
81
|
+
</NavigationMenu.Item>
|
|
82
|
+
)
|
|
83
|
+
)}
|
|
84
|
+
</NavigationMenu.List>
|
|
85
|
+
|
|
86
|
+
<NavigationMenu.Viewport />
|
|
87
|
+
|
|
88
|
+
<NavigationMenu.MobileBrand>{brand}</NavigationMenu.MobileBrand>
|
|
89
|
+
|
|
90
|
+
<NavigationMenu.MobileContent>
|
|
91
|
+
{/* Render all headerNav items in the mobile drawer */}
|
|
92
|
+
<NavigationMenu.MobileSection>
|
|
93
|
+
{headerNav.map((entry) =>
|
|
94
|
+
isDropdown(entry) ? (
|
|
95
|
+
entry.items.map((child) => (
|
|
96
|
+
<NavigationMenu.Link key={child.href} href={child.href} asChild>
|
|
97
|
+
{renderLink({ href: child.href, label: child.label })}
|
|
98
|
+
</NavigationMenu.Link>
|
|
99
|
+
))
|
|
100
|
+
) : (
|
|
101
|
+
<NavigationMenu.Link key={entry.href} href={entry.href} asChild>
|
|
102
|
+
{renderLink({ href: entry.href, label: entry.label })}
|
|
103
|
+
</NavigationMenu.Link>
|
|
104
|
+
)
|
|
105
|
+
)}
|
|
106
|
+
</NavigationMenu.MobileSection>
|
|
107
|
+
|
|
108
|
+
{mobileSections.map((section) => (
|
|
109
|
+
<NavigationMenu.MobileSection key={section.title} label={section.title}>
|
|
110
|
+
{section.items.map((item) => (
|
|
111
|
+
<NavigationMenu.Link key={item.href} href={item.href} asChild>
|
|
112
|
+
{renderLink({ href: item.href, label: item.label })}
|
|
113
|
+
</NavigationMenu.Link>
|
|
114
|
+
))}
|
|
115
|
+
</NavigationMenu.MobileSection>
|
|
116
|
+
))}
|
|
117
|
+
</NavigationMenu.MobileContent>
|
|
118
|
+
</NavigationMenu>
|
|
119
|
+
|
|
120
|
+
<Header.Spacer />
|
|
121
|
+
|
|
122
|
+
<Header.Search>
|
|
123
|
+
<DocsSearchCommand searchItems={searchItems} onSelect={onSearchSelect} />
|
|
124
|
+
</Header.Search>
|
|
125
|
+
|
|
126
|
+
<Header.Actions>{actions}</Header.Actions>
|
|
127
|
+
</Header>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
interface DocsPageAsideContextValue {
|
|
7
|
+
asideVisible: boolean;
|
|
8
|
+
setAsideVisible: (visible: boolean) => void;
|
|
9
|
+
asideWidth: string;
|
|
10
|
+
setAsideWidth: (width: string) => void;
|
|
11
|
+
asideContainer: HTMLDivElement | null;
|
|
12
|
+
setAsideContainer: (container: HTMLDivElement | null) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DocsPageAsideContext = React.createContext<DocsPageAsideContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
export function DocsPageAsideProvider({
|
|
18
|
+
children,
|
|
19
|
+
defaultWidth = '320px',
|
|
20
|
+
}: {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
defaultWidth?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const [asideVisible, setAsideVisible] = React.useState(false);
|
|
25
|
+
const [asideWidth, setAsideWidth] = React.useState(defaultWidth);
|
|
26
|
+
const [asideContainer, setAsideContainer] = React.useState<HTMLDivElement | null>(null);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DocsPageAsideContext.Provider
|
|
30
|
+
value={{
|
|
31
|
+
asideVisible,
|
|
32
|
+
setAsideVisible,
|
|
33
|
+
asideWidth,
|
|
34
|
+
setAsideWidth,
|
|
35
|
+
asideContainer,
|
|
36
|
+
setAsideContainer,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</DocsPageAsideContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useDocsPageAside() {
|
|
45
|
+
const context = React.useContext(DocsPageAsideContext);
|
|
46
|
+
if (!context) {
|
|
47
|
+
throw new Error('useDocsPageAside must be used within DocsPageAsideProvider');
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function DocsPageAsidePortal({ children, width = '320px' }: { children: React.ReactNode; width?: string }) {
|
|
53
|
+
const { setAsideVisible, setAsideWidth, asideContainer } = useDocsPageAside();
|
|
54
|
+
|
|
55
|
+
React.useLayoutEffect(() => {
|
|
56
|
+
setAsideVisible(true);
|
|
57
|
+
setAsideWidth(width);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
setAsideVisible(false);
|
|
61
|
+
};
|
|
62
|
+
}, [setAsideVisible, setAsideWidth, width]);
|
|
63
|
+
|
|
64
|
+
if (!asideContainer) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return createPortal(children, asideContainer);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface DocsPageAsideHostProps {
|
|
72
|
+
className?: string;
|
|
73
|
+
style?: React.CSSProperties;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function DocsPageAsideHost({ className, style }: DocsPageAsideHostProps) {
|
|
77
|
+
const { setAsideContainer } = useDocsPageAside();
|
|
78
|
+
|
|
79
|
+
const asideContainerCallback = React.useCallback(
|
|
80
|
+
(node: HTMLDivElement | null) => {
|
|
81
|
+
setAsideContainer(node);
|
|
82
|
+
},
|
|
83
|
+
[setAsideContainer]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div ref={asideContainerCallback} className={className} style={{ height: '100%', ...style }} />
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
sidebarDefaultCollapsed?: boolean;
|
|
16
|
+
sidebarAriaLabel?: string;
|
|
17
|
+
mainId?: string;
|
|
18
|
+
mainAriaLabel?: string;
|
|
19
|
+
mainPadding?: 'none' | 'sm' | 'md' | 'lg';
|
|
20
|
+
mainClassName?: string;
|
|
21
|
+
aside?: ReactNode;
|
|
22
|
+
asideWidth?: string;
|
|
23
|
+
useAsidePortal?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function DocsPageShellInner({
|
|
27
|
+
header,
|
|
28
|
+
sidebar,
|
|
29
|
+
children,
|
|
30
|
+
sidebarWidth = '260px',
|
|
31
|
+
sidebarCollapsible = 'offcanvas',
|
|
32
|
+
sidebarDefaultCollapsed,
|
|
33
|
+
sidebarAriaLabel = 'Documentation sidebar',
|
|
34
|
+
mainId = 'main-content',
|
|
35
|
+
mainAriaLabel = 'Documentation content',
|
|
36
|
+
mainPadding = 'lg',
|
|
37
|
+
mainClassName,
|
|
38
|
+
aside,
|
|
39
|
+
asideWidth = '320px',
|
|
40
|
+
useAsidePortal = false,
|
|
41
|
+
}: DocsPageShellProps) {
|
|
42
|
+
return (
|
|
43
|
+
<AppShell>
|
|
44
|
+
<AppShell.Header>{header}</AppShell.Header>
|
|
45
|
+
|
|
46
|
+
<AppShell.Sidebar
|
|
47
|
+
width={sidebarWidth}
|
|
48
|
+
collapsible={sidebarCollapsible}
|
|
49
|
+
defaultCollapsed={sidebarDefaultCollapsed}
|
|
50
|
+
aria-label={sidebarAriaLabel}
|
|
51
|
+
>
|
|
52
|
+
{sidebar}
|
|
53
|
+
</AppShell.Sidebar>
|
|
54
|
+
|
|
55
|
+
<AppShell.Main
|
|
56
|
+
id={mainId}
|
|
57
|
+
aria-label={mainAriaLabel}
|
|
58
|
+
padding={mainPadding}
|
|
59
|
+
className={mainClassName}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</AppShell.Main>
|
|
63
|
+
|
|
64
|
+
{useAsidePortal ? <DocsPageAsideHost /> : null}
|
|
65
|
+
{!useAsidePortal && aside ? <AppShell.Aside width={asideWidth}>{aside}</AppShell.Aside> : null}
|
|
66
|
+
</AppShell>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function DocsPageShellPortalInner({
|
|
71
|
+
header,
|
|
72
|
+
sidebar,
|
|
73
|
+
children,
|
|
74
|
+
sidebarWidth = '260px',
|
|
75
|
+
sidebarCollapsible = 'offcanvas',
|
|
76
|
+
sidebarDefaultCollapsed,
|
|
77
|
+
sidebarAriaLabel = 'Documentation sidebar',
|
|
78
|
+
mainId = 'main-content',
|
|
79
|
+
mainAriaLabel = 'Documentation content',
|
|
80
|
+
mainPadding = 'lg',
|
|
81
|
+
mainClassName,
|
|
82
|
+
}: DocsPageShellProps) {
|
|
83
|
+
const { asideVisible, asideWidth } = useDocsPageAside();
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<AppShell>
|
|
87
|
+
<AppShell.Header>{header}</AppShell.Header>
|
|
88
|
+
|
|
89
|
+
<AppShell.Sidebar
|
|
90
|
+
width={sidebarWidth}
|
|
91
|
+
collapsible={sidebarCollapsible}
|
|
92
|
+
defaultCollapsed={sidebarDefaultCollapsed}
|
|
93
|
+
aria-label={sidebarAriaLabel}
|
|
94
|
+
>
|
|
95
|
+
{sidebar}
|
|
96
|
+
</AppShell.Sidebar>
|
|
97
|
+
|
|
98
|
+
<AppShell.Main
|
|
99
|
+
id={mainId}
|
|
100
|
+
aria-label={mainAriaLabel}
|
|
101
|
+
padding={mainPadding}
|
|
102
|
+
className={mainClassName}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</AppShell.Main>
|
|
106
|
+
|
|
107
|
+
<AppShell.Aside width={asideWidth} visible={asideVisible} aria-label="On-page controls">
|
|
108
|
+
<DocsPageAsideHost />
|
|
109
|
+
</AppShell.Aside>
|
|
110
|
+
</AppShell>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function DocsPageShell(props: DocsPageShellProps) {
|
|
115
|
+
if (props.useAsidePortal) {
|
|
116
|
+
return (
|
|
117
|
+
<DocsPageAsideProvider defaultWidth={props.asideWidth}>
|
|
118
|
+
<DocsPageShellPortalInner {...props} />
|
|
119
|
+
</DocsPageAsideProvider>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return <DocsPageShellInner {...props} />;
|
|
124
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Command, Dialog, Button, Text, Stack } from '@fragments-sdk/ui';
|
|
4
|
+
import { useEffect, useMemo, useState, useCallback } 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 docs...',
|
|
18
|
+
maxResults = 9999,
|
|
19
|
+
}: DocsSearchCommandProps) {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Cmd+K / Ctrl+K to open
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
25
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
setOpen((prev) => !prev);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
32
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
// Group items by section
|
|
36
|
+
const grouped = useMemo(() => {
|
|
37
|
+
const map = new Map<string, SearchItem[]>();
|
|
38
|
+
for (const item of searchItems) {
|
|
39
|
+
const section = item.section || 'Navigation';
|
|
40
|
+
if (!map.has(section)) map.set(section, []);
|
|
41
|
+
map.get(section)!.push(item);
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}, [searchItems]);
|
|
45
|
+
|
|
46
|
+
const handleSelect = useCallback(
|
|
47
|
+
(item: SearchItem) => {
|
|
48
|
+
setOpen(false);
|
|
49
|
+
onSelect(item);
|
|
50
|
+
},
|
|
51
|
+
[onSelect],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<Button
|
|
57
|
+
variant="secondary"
|
|
58
|
+
size="sm"
|
|
59
|
+
onClick={() => setOpen(true)}
|
|
60
|
+
aria-label="Search documentation"
|
|
61
|
+
style={{ minWidth: 160, justifyContent: 'space-between' }}
|
|
62
|
+
>
|
|
63
|
+
<Text size="sm" color="secondary">Search...</Text>
|
|
64
|
+
<Text size="xs" color="secondary" font="mono">⌘K</Text>
|
|
65
|
+
</Button>
|
|
66
|
+
|
|
67
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
68
|
+
<Dialog.Content size="sm" style={{ padding: 0, overflow: 'hidden' }}>
|
|
69
|
+
<Command
|
|
70
|
+
filter={(value, search) => {
|
|
71
|
+
if (value.toLowerCase().includes(search.toLowerCase())) return 1;
|
|
72
|
+
return 0;
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<Command.Input placeholder={placeholder} />
|
|
76
|
+
<Command.List>
|
|
77
|
+
<Command.Empty>No results found.</Command.Empty>
|
|
78
|
+
{[...grouped.entries()].map(([section, items]) => (
|
|
79
|
+
<Command.Group key={section} heading={section}>
|
|
80
|
+
{items.slice(0, maxResults).map((item) => (
|
|
81
|
+
<Command.Item
|
|
82
|
+
key={`${item.section}:${item.href}`}
|
|
83
|
+
value={`${item.label} ${item.section}`}
|
|
84
|
+
onItemSelect={() => handleSelect(item)}
|
|
85
|
+
>
|
|
86
|
+
<Stack direction="row" align="center" justify="between" style={{ width: '100%' }}>
|
|
87
|
+
<Text size="sm">{item.label}</Text>
|
|
88
|
+
</Stack>
|
|
89
|
+
</Command.Item>
|
|
90
|
+
))}
|
|
91
|
+
</Command.Group>
|
|
92
|
+
))}
|
|
93
|
+
</Command.List>
|
|
94
|
+
</Command>
|
|
95
|
+
</Dialog.Content>
|
|
96
|
+
</Dialog>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -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,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,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
|
+
}
|