@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.
Files changed (141) hide show
  1. package/LICENSE +84 -0
  2. package/index.html +28 -0
  3. package/package.json +71 -0
  4. package/src/__tests__/a11y-fixes.test.ts +358 -0
  5. package/src/__tests__/jsx-parser.test.ts +502 -0
  6. package/src/__tests__/render-utils.test.ts +232 -0
  7. package/src/__tests__/style-utils.test.ts +404 -0
  8. package/src/app/index.ts +1 -0
  9. package/src/assets/fragments-logo.ts +4 -0
  10. package/src/assets/fragments_logo.png +0 -0
  11. package/src/components/AccessibilityPanel.tsx +1457 -0
  12. package/src/components/ActionCapture.tsx +172 -0
  13. package/src/components/ActionsPanel.tsx +332 -0
  14. package/src/components/AllVariantsPreview.tsx +78 -0
  15. package/src/components/App.tsx +604 -0
  16. package/src/components/BottomPanel.tsx +288 -0
  17. package/src/components/CodePanel.naming.test.tsx +59 -0
  18. package/src/components/CodePanel.tsx +118 -0
  19. package/src/components/CommandPalette.tsx +392 -0
  20. package/src/components/ComponentDocView.tsx +164 -0
  21. package/src/components/ComponentGraph.tsx +380 -0
  22. package/src/components/ComponentHeader.tsx +88 -0
  23. package/src/components/ContractPanel.tsx +241 -0
  24. package/src/components/DeviceMockup.tsx +156 -0
  25. package/src/components/EmptyVariantMessage.tsx +54 -0
  26. package/src/components/ErrorBoundary.tsx +97 -0
  27. package/src/components/FigmaEmbed.tsx +238 -0
  28. package/src/components/FragmentEditor.tsx +525 -0
  29. package/src/components/FragmentRenderer.tsx +61 -0
  30. package/src/components/HeaderSearch.tsx +24 -0
  31. package/src/components/HealthDashboard.tsx +441 -0
  32. package/src/components/HmrStatusIndicator.tsx +61 -0
  33. package/src/components/Icons.tsx +479 -0
  34. package/src/components/InteractionsPanel.tsx +757 -0
  35. package/src/components/IsolatedPreviewFrame.tsx +390 -0
  36. package/src/components/IsolatedRender.tsx +113 -0
  37. package/src/components/KeyboardShortcutsHelp.tsx +53 -0
  38. package/src/components/LandingPage.tsx +420 -0
  39. package/src/components/Layout.tsx +27 -0
  40. package/src/components/LeftSidebar.tsx +472 -0
  41. package/src/components/LoadErrorMessage.tsx +102 -0
  42. package/src/components/MultiViewportPreview.tsx +527 -0
  43. package/src/components/NoVariantsMessage.tsx +59 -0
  44. package/src/components/PanelShell.tsx +161 -0
  45. package/src/components/PerformancePanel.tsx +304 -0
  46. package/src/components/PreviewArea.tsx +254 -0
  47. package/src/components/PreviewAside.tsx +168 -0
  48. package/src/components/PreviewFrameHost.tsx +304 -0
  49. package/src/components/PreviewToolbar.tsx +80 -0
  50. package/src/components/PropsEditor.tsx +506 -0
  51. package/src/components/PropsTable.tsx +111 -0
  52. package/src/components/RelationsSection.tsx +88 -0
  53. package/src/components/ResizablePanel.tsx +271 -0
  54. package/src/components/RightSidebar.tsx +102 -0
  55. package/src/components/RuntimeToolsRegistrar.tsx +17 -0
  56. package/src/components/ScreenshotButton.tsx +90 -0
  57. package/src/components/ShadowPreview.tsx +204 -0
  58. package/src/components/Sidebar.tsx +169 -0
  59. package/src/components/SkeletonLoader.tsx +161 -0
  60. package/src/components/ThemeProvider.tsx +42 -0
  61. package/src/components/Toast.tsx +3 -0
  62. package/src/components/TokenStylePanel.tsx +699 -0
  63. package/src/components/TopToolbar.tsx +159 -0
  64. package/src/components/Untitled +1 -0
  65. package/src/components/UsageSection.tsx +95 -0
  66. package/src/components/VariantMatrix.tsx +391 -0
  67. package/src/components/VariantRenderer.tsx +131 -0
  68. package/src/components/VariantTabs.tsx +40 -0
  69. package/src/components/ViewerHeader.tsx +69 -0
  70. package/src/components/ViewerStateSync.tsx +52 -0
  71. package/src/components/ViewportSelector.tsx +172 -0
  72. package/src/components/WebMCPDevTools.tsx +503 -0
  73. package/src/components/WebMCPIntegration.tsx +47 -0
  74. package/src/components/WebMCPStatusIndicator.tsx +60 -0
  75. package/src/components/_future/CreatePage.tsx +835 -0
  76. package/src/components/viewer-utils.ts +16 -0
  77. package/src/composition-renderer.ts +381 -0
  78. package/src/constants/index.ts +1 -0
  79. package/src/constants/ui.ts +166 -0
  80. package/src/entry.tsx +335 -0
  81. package/src/hooks/index.ts +2 -0
  82. package/src/hooks/useA11yCache.ts +383 -0
  83. package/src/hooks/useA11yService.ts +364 -0
  84. package/src/hooks/useActions.ts +138 -0
  85. package/src/hooks/useAppState.ts +147 -0
  86. package/src/hooks/useCompiledFragments.ts +42 -0
  87. package/src/hooks/useFigmaIntegration.ts +132 -0
  88. package/src/hooks/useHmrStatus.ts +109 -0
  89. package/src/hooks/useKeyboardShortcuts.ts +270 -0
  90. package/src/hooks/usePreviewBridge.ts +347 -0
  91. package/src/hooks/useScrollSpy.ts +78 -0
  92. package/src/hooks/useShadowStyles.ts +221 -0
  93. package/src/hooks/useUrlState.ts +318 -0
  94. package/src/hooks/useViewSettings.ts +111 -0
  95. package/src/intelligence/healthReport.ts +505 -0
  96. package/src/intelligence/styleDrift.ts +340 -0
  97. package/src/intelligence/usageScanner.ts +309 -0
  98. package/src/jsx-parser.ts +486 -0
  99. package/src/preview-frame-entry.tsx +25 -0
  100. package/src/preview-frame.html +148 -0
  101. package/src/render-template.html +68 -0
  102. package/src/render-utils.ts +311 -0
  103. package/src/shared/ComponentDocContent.module.scss +10 -0
  104. package/src/shared/ComponentDocContent.module.scss.d.ts +2 -0
  105. package/src/shared/ComponentDocContent.tsx +274 -0
  106. package/src/shared/DocsHeaderBar.tsx +129 -0
  107. package/src/shared/DocsPageAsideHost.tsx +89 -0
  108. package/src/shared/DocsPageShell.tsx +124 -0
  109. package/src/shared/DocsSearchCommand.tsx +99 -0
  110. package/src/shared/DocsSidebarNav.tsx +66 -0
  111. package/src/shared/PropsTable.module.scss +68 -0
  112. package/src/shared/PropsTable.module.scss.d.ts +2 -0
  113. package/src/shared/PropsTable.tsx +76 -0
  114. package/src/shared/VariantPreviewCard.module.scss +114 -0
  115. package/src/shared/VariantPreviewCard.module.scss.d.ts +2 -0
  116. package/src/shared/VariantPreviewCard.tsx +137 -0
  117. package/src/shared/docs-data/index.ts +32 -0
  118. package/src/shared/docs-data/mcp-configs.ts +72 -0
  119. package/src/shared/docs-data/palettes.ts +75 -0
  120. package/src/shared/docs-data/setup-examples.ts +55 -0
  121. package/src/shared/docs-layout.scss +28 -0
  122. package/src/shared/docs-layout.scss.d.ts +2 -0
  123. package/src/shared/index.ts +34 -0
  124. package/src/shared/types.ts +53 -0
  125. package/src/style-utils.ts +414 -0
  126. package/src/styles/globals.css +278 -0
  127. package/src/types/a11y.ts +197 -0
  128. package/src/utils/a11y-fixes.ts +509 -0
  129. package/src/utils/actionExport.ts +372 -0
  130. package/src/utils/colorSchemes.ts +201 -0
  131. package/src/utils/contrast.ts +246 -0
  132. package/src/utils/detectRelationships.ts +256 -0
  133. package/src/webmcp/__tests__/analytics.test.ts +108 -0
  134. package/src/webmcp/analytics.ts +165 -0
  135. package/src/webmcp/index.ts +3 -0
  136. package/src/webmcp/posthog-bridge.ts +39 -0
  137. package/src/webmcp/runtime-tools.ts +152 -0
  138. package/src/webmcp/scan-utils.ts +135 -0
  139. package/src/webmcp/use-tool-analytics.ts +69 -0
  140. package/src/webmcp/viewer-state.ts +45 -0
  141. 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,2 @@
1
+ declare const styles: Record<string, string>;
2
+ export default styles;
@@ -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
+ }