@fragments-sdk/viewer 0.2.1 → 0.2.3

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 (60) hide show
  1. package/package.json +10 -3
  2. package/src/components/App.tsx +67 -4
  3. package/src/components/BottomPanel.tsx +31 -1
  4. package/src/components/ComponentGraph.tsx +1 -1
  5. package/src/components/EmptyVariantMessage.tsx +1 -1
  6. package/src/components/ErrorBoundary.tsx +2 -4
  7. package/src/components/FragmentRenderer.tsx +27 -4
  8. package/src/components/HeaderSearch.tsx +1 -1
  9. package/src/components/InteractionsPanel.tsx +5 -5
  10. package/src/components/LoadErrorMessage.tsx +26 -30
  11. package/src/components/NoVariantsMessage.tsx +1 -1
  12. package/src/components/PanelShell.tsx +1 -1
  13. package/src/components/PerformancePanel.tsx +4 -4
  14. package/src/components/PreviewArea.tsx +6 -0
  15. package/src/components/PropsEditor.tsx +33 -17
  16. package/src/components/SkeletonLoader.tsx +0 -1
  17. package/src/components/TokenStylePanel.tsx +3 -3
  18. package/src/components/TopToolbar.tsx +1 -1
  19. package/src/components/VariantMatrix.tsx +11 -1
  20. package/src/components/ViewerHeader.tsx +1 -1
  21. package/src/components/WebMCPDevTools.tsx +2 -2
  22. package/src/entry.tsx +4 -6
  23. package/src/hooks/useAppState.ts +1 -1
  24. package/src/preview-frame-entry.tsx +0 -2
  25. package/src/shared/DocsHeaderBar.module.scss +174 -0
  26. package/src/shared/DocsHeaderBar.tsx +149 -14
  27. package/src/shared/DocsSidebarNav.tsx +3 -3
  28. package/src/shared/index.ts +4 -1
  29. package/src/shared/types.ts +29 -3
  30. package/src/style-utils.ts +1 -1
  31. package/src/webmcp/runtime-tools.ts +1 -1
  32. package/tsconfig.json +2 -2
  33. package/src/assets/fragments-logo.ts +0 -4
  34. package/src/components/ActionsPanel.tsx +0 -332
  35. package/src/components/ComponentHeader.tsx +0 -88
  36. package/src/components/ContractPanel.tsx +0 -241
  37. package/src/components/FragmentEditor.tsx +0 -525
  38. package/src/components/HmrStatusIndicator.tsx +0 -61
  39. package/src/components/LandingPage.tsx +0 -420
  40. package/src/components/PropsTable.tsx +0 -111
  41. package/src/components/RelationsSection.tsx +0 -88
  42. package/src/components/ResizablePanel.tsx +0 -271
  43. package/src/components/RightSidebar.tsx +0 -102
  44. package/src/components/ScreenshotButton.tsx +0 -90
  45. package/src/components/Sidebar.tsx +0 -169
  46. package/src/components/UsageSection.tsx +0 -95
  47. package/src/components/VariantTabs.tsx +0 -40
  48. package/src/components/ViewportSelector.tsx +0 -172
  49. package/src/components/_future/CreatePage.tsx +0 -835
  50. package/src/composition-renderer.ts +0 -381
  51. package/src/constants/index.ts +0 -1
  52. package/src/hooks/index.ts +0 -2
  53. package/src/hooks/useHmrStatus.ts +0 -109
  54. package/src/hooks/useScrollSpy.ts +0 -78
  55. package/src/intelligence/healthReport.ts +0 -505
  56. package/src/intelligence/styleDrift.ts +0 -340
  57. package/src/intelligence/usageScanner.ts +0 -309
  58. package/src/utils/actionExport.ts +0 -372
  59. package/src/utils/colorSchemes.ts +0 -201
  60. package/src/webmcp/index.ts +0 -3
@@ -14,7 +14,6 @@ export function AppSkeleton() {
14
14
  <div
15
15
  style={{
16
16
  display: 'grid',
17
- minHeight: '100vh',
18
17
  minHeight: '100dvh',
19
18
  gridTemplateRows: '56px 1fr',
20
19
  gridTemplateColumns: '260px 1fr 240px',
@@ -332,7 +332,7 @@ export function TokenStylePanel({
332
332
  <Stack direction="row" align="center" gap="sm">
333
333
  {tokenData && (
334
334
  <Badge
335
- variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : 'danger'}
335
+ variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : 'error'}
336
336
  >
337
337
  {compliancePercent}% token compliance
338
338
  </Badge>
@@ -343,7 +343,7 @@ export function TokenStylePanel({
343
343
  </Badge>
344
344
 
345
345
  {hardcodedCount > 0 && (
346
- <Badge variant="danger">
346
+ <Badge variant="error">
347
347
  {hardcodedCount} hardcoded
348
348
  </Badge>
349
349
  )}
@@ -414,7 +414,7 @@ export function TokenStylePanel({
414
414
  {prop.rendered}
415
415
  </span>
416
416
  {prop.isHardcoded && (
417
- <Badge variant="danger">HC</Badge>
417
+ <Badge variant="error">HC</Badge>
418
418
  )}
419
419
  </div>
420
420
  </td>
@@ -32,7 +32,7 @@ interface TopToolbarProps {
32
32
  figmaUrl?: string;
33
33
  searchQuery: string;
34
34
  onSearchChange: (value: string) => void;
35
- searchInputRef: RefObject<HTMLInputElement>;
35
+ searchInputRef: RefObject<HTMLInputElement | null>;
36
36
  }
37
37
 
38
38
  export function TopToolbar({
@@ -33,6 +33,10 @@ interface VariantMatrixProps {
33
33
  previewTheme: "light" | "dark";
34
34
  /** Whether to use iframe isolation */
35
35
  useIframeIsolation?: boolean;
36
+ /** Variant index currently focused in the main viewer */
37
+ activeVariantIndex?: number;
38
+ /** Args overrides to apply to the focused variant only */
39
+ variantArgsOverrides?: Record<string, unknown>;
36
40
  /** Callback when a variant is clicked to focus on it */
37
41
  onSelectVariant?: (index: number) => void;
38
42
  }
@@ -63,6 +67,8 @@ export function VariantMatrix({
63
67
  zoom,
64
68
  previewTheme,
65
69
  useIframeIsolation = true,
70
+ activeVariantIndex,
71
+ variantArgsOverrides,
66
72
  onSelectVariant,
67
73
  }: VariantMatrixProps) {
68
74
  const [gridSize, setGridSize] = useState<GridSize>("medium");
@@ -173,6 +179,7 @@ export function VariantMatrix({
173
179
  isHovered={hoveredIndex === index}
174
180
  onHover={() => setHoveredIndex(index)}
175
181
  onLeave={() => setHoveredIndex(null)}
182
+ argsOverrides={index === activeVariantIndex ? variantArgsOverrides : undefined}
176
183
  onClick={() => onSelectVariant?.(index)}
177
184
  />
178
185
  );
@@ -204,6 +211,7 @@ export function VariantMatrix({
204
211
  isHovered={hoveredIndex === index}
205
212
  onHover={() => setHoveredIndex(index)}
206
213
  onLeave={() => setHoveredIndex(null)}
214
+ argsOverrides={index === activeVariantIndex ? variantArgsOverrides : undefined}
207
215
  onClick={() => onSelectVariant?.(index)}
208
216
  />
209
217
  ))}
@@ -226,6 +234,7 @@ interface VariantCardProps {
226
234
  isHovered: boolean;
227
235
  onHover: () => void;
228
236
  onLeave: () => void;
237
+ argsOverrides?: Record<string, unknown>;
229
238
  onClick: () => void;
230
239
  }
231
240
 
@@ -241,6 +250,7 @@ function VariantCard({
241
250
  isHovered,
242
251
  onHover,
243
252
  onLeave,
253
+ argsOverrides,
244
254
  onClick,
245
255
  }: VariantCardProps) {
246
256
  return (
@@ -355,7 +365,7 @@ function VariantCard({
355
365
  </Text>
356
366
  }
357
367
  >
358
- <FragmentRenderer variant={variant}>
368
+ <FragmentRenderer variant={variant} argsOverrides={argsOverrides}>
359
369
  {(content, isLoading, error) => {
360
370
  if (isLoading) {
361
371
  return (
@@ -17,7 +17,7 @@ interface ViewerHeaderProps {
17
17
  showHealth: boolean;
18
18
  searchQuery: string;
19
19
  onSearchChange: (value: string) => void;
20
- searchInputRef: RefObject<HTMLInputElement>;
20
+ searchInputRef: RefObject<HTMLInputElement | null>;
21
21
  }
22
22
 
23
23
  export function ViewerHeader({
@@ -151,7 +151,7 @@ export function WebMCPDevTools() {
151
151
  <>
152
152
  <Select
153
153
  value={selectedTool}
154
- onValueChange={setSelectedTool}
154
+ onValueChange={(value) => setSelectedTool(value ?? "")}
155
155
  placeholder="Select a tool"
156
156
  >
157
157
  <Select.Trigger />
@@ -246,7 +246,7 @@ export function WebMCPDevTools() {
246
246
  <Stack direction="column" gap="md" style={{ paddingTop: "12px" }}>
247
247
  <Select
248
248
  value={selectedTool}
249
- onValueChange={setSelectedTool}
249
+ onValueChange={(value) => setSelectedTool(value ?? "")}
250
250
  placeholder="Select a tool"
251
251
  >
252
252
  <Select.Trigger />
package/src/entry.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRoot, type Root } from "react-dom/client";
2
2
  import { Component, type ReactNode, type ErrorInfo } from "react";
3
+ import type { FragmentDefinition, FragmentsConfig } from "@fragments-sdk/core";
3
4
  import { App } from "./components/App.js";
4
5
  import { ThemeProvider } from "./components/ThemeProvider.js";
5
6
  import { ToastProvider } from "./components/Toast.js";
@@ -134,9 +135,9 @@ declare global {
134
135
  interface Window {
135
136
  __FRAGMENTS__?: Array<{
136
137
  path: string;
137
- fragment: import("../core/index.js").FragmentDefinition;
138
+ fragment: FragmentDefinition;
138
139
  }>;
139
- __FRAGMENTS_CONFIG__?: import("../core/index.js").FragmentsConfig;
140
+ __FRAGMENTS_CONFIG__?: FragmentsConfig;
140
141
  __FRAGMENTS_PACKAGE_NAME__?: string | null;
141
142
  __FRAGMENTS_ERROR__?: string;
142
143
  }
@@ -144,7 +145,7 @@ declare global {
144
145
 
145
146
  type FragmentItem = {
146
147
  path: string;
147
- fragment: import("../core/index.js").FragmentDefinition;
148
+ fragment: FragmentDefinition;
148
149
  };
149
150
 
150
151
  // Initialize fragments from window or empty array
@@ -309,13 +310,10 @@ if (rootElement) {
309
310
  }
310
311
 
311
312
  // HMR support for entry file and a11y invalidation
312
- // @ts-expect-error Vite HMR types
313
313
  if (import.meta.hot) {
314
- // @ts-expect-error Vite HMR types
315
314
  import.meta.hot.accept();
316
315
 
317
316
  // Subscribe to a11y invalidation events from Vite plugin
318
- // @ts-expect-error Vite HMR types
319
317
  import.meta.hot.on("fragments:a11y-invalidate", (data: {
320
318
  components: string[];
321
319
  file: string;
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useReducer, useCallback, useMemo } from 'react';
7
7
 
8
- export type ActivePanel = 'styles' | 'accessibility' | 'interactions' | 'graph' | 'performance';
8
+ export type ActivePanel = 'styles' | 'controls' | 'accessibility' | 'interactions' | 'graph' | 'performance';
9
9
 
10
10
  interface AppUIState {
11
11
  activePanel: ActivePanel;
@@ -18,8 +18,6 @@ if (rootElement) {
18
18
  }
19
19
 
20
20
  // HMR support
21
- // @ts-expect-error Vite HMR types
22
21
  if (import.meta.hot) {
23
- // @ts-expect-error Vite HMR types
24
22
  import.meta.hot.accept();
25
23
  }
@@ -0,0 +1,174 @@
1
+ // ============================================
2
+ // Mega-Menu Grid
3
+ // ============================================
4
+
5
+ .megaMenuGrid {
6
+ display: grid;
7
+ gap: 0; // divider handles visual separation
8
+ padding: var(--fui-space-5) var(--fui-space-5);
9
+ min-width: 540px;
10
+ }
11
+
12
+ // ============================================
13
+ // Sections & Divider
14
+ // ============================================
15
+
16
+ .megaMenuSection {
17
+ display: flex;
18
+ flex-direction: column;
19
+ padding: 0 var(--fui-space-2);
20
+
21
+ &:first-child {
22
+ padding-left: 0;
23
+ }
24
+
25
+ &:last-child {
26
+ padding-right: 0;
27
+ }
28
+ }
29
+
30
+ .megaMenuDivider {
31
+ width: 1px;
32
+ background-color: var(--fui-border-default);
33
+ align-self: stretch;
34
+ }
35
+
36
+ .megaMenuSectionTitle {
37
+ font-size: 0.714rem; // ~10px — smaller than xs for strong hierarchy
38
+ font-weight: var(--fui-font-weight-semibold);
39
+ color: var(--fui-text-tertiary);
40
+ text-transform: uppercase;
41
+ letter-spacing: 0.08em;
42
+ padding: var(--fui-space-0-5) var(--fui-space-2) var(--fui-space-2);
43
+ }
44
+
45
+ // ============================================
46
+ // Menu Items (overrides NavigationMenu .link)
47
+ // Specificity: .megaMenuSection .megaMenuItem = 0,2,0
48
+ // NavigationMenu .link = 0,1,0 — we always win
49
+ // ============================================
50
+
51
+ .megaMenuSection .megaMenuItem {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: var(--fui-space-3);
55
+ padding: var(--fui-space-1) var(--fui-space-2);
56
+ border-radius: var(--fui-radius-md);
57
+ text-decoration: none;
58
+ white-space: normal;
59
+ min-height: auto;
60
+ color: inherit;
61
+ transition: background-color 150ms ease, color 150ms ease;
62
+
63
+ &:hover {
64
+ background-color: var(--fui-bg-hover);
65
+
66
+ .megaMenuItemDesc {
67
+ color: var(--fui-text-secondary);
68
+ }
69
+
70
+ .packageLogo {
71
+ transform: translateY(-1px);
72
+ box-shadow:
73
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
74
+ 0 3px 8px -2px rgba(0, 0, 0, 0.4),
75
+ 0 0 0 1px rgba(255, 255, 255, 0.08);
76
+ }
77
+ }
78
+
79
+ &:focus-visible {
80
+ outline: 2px solid var(--fui-color-accent, #6366f1);
81
+ outline-offset: -2px;
82
+ border-radius: var(--fui-radius-md);
83
+ }
84
+
85
+ @media (prefers-reduced-motion: reduce) {
86
+ transition: none;
87
+
88
+ &:hover .packageLogo {
89
+ transform: none;
90
+ }
91
+ }
92
+ }
93
+
94
+ // Active state (current page) — override NavigationMenu .linkActive too
95
+ .megaMenuSection .megaMenuItemActive {
96
+ background-color: var(--fui-bg-secondary);
97
+
98
+ &:hover {
99
+ background-color: var(--fui-bg-secondary);
100
+ }
101
+ }
102
+
103
+ // ============================================
104
+ // Item Internals
105
+ // ============================================
106
+
107
+ .megaMenuItemBody {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 2px; // tight coupling of title + description
111
+ min-width: 0;
112
+ }
113
+
114
+ .megaMenuItemTitle {
115
+ font-size: var(--fui-font-size-sm);
116
+ font-weight: var(--fui-font-weight-medium);
117
+ color: var(--fui-text-primary);
118
+ line-height: 1.3;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: var(--fui-space-1);
122
+ }
123
+
124
+ .megaMenuItemDesc {
125
+ font-size: var(--fui-font-size-xs);
126
+ color: var(--fui-text-tertiary);
127
+ line-height: 1.4;
128
+ transition: color 150ms ease;
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ transition: none;
132
+ }
133
+ }
134
+
135
+ // ============================================
136
+ // Badge — tiny subordinate pill
137
+ // ============================================
138
+
139
+ .megaMenuBadge {
140
+ display: inline-flex;
141
+ align-items: center;
142
+ font-size: 9px;
143
+ font-weight: var(--fui-font-weight-semibold);
144
+ color: var(--fui-text-tertiary);
145
+ text-transform: uppercase;
146
+ letter-spacing: 0.04em;
147
+ line-height: 1;
148
+ padding: 2px 5px;
149
+ border-radius: var(--fui-radius-full);
150
+ border: 1px solid var(--fui-border-default);
151
+ white-space: nowrap;
152
+ flex-shrink: 0;
153
+ }
154
+
155
+ // ============================================
156
+ // Package Logo (wave gradient + abbreviation)
157
+ // ============================================
158
+
159
+ .packageLogo {
160
+ flex-shrink: 0;
161
+ width: 36px;
162
+ height: 36px;
163
+ border-radius: var(--fui-radius-md);
164
+ overflow: hidden;
165
+ box-shadow:
166
+ inset 0 1px 0 rgba(255, 255, 255, 0.15),
167
+ 0 2px 6px -1px rgba(0, 0, 0, 0.3),
168
+ 0 0 0 1px rgba(255, 255, 255, 0.06);
169
+ transition: box-shadow 150ms ease, transform 150ms ease;
170
+
171
+ @media (prefers-reduced-motion: reduce) {
172
+ transition: none;
173
+ }
174
+ }
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { Header, NavigationMenu } from '@fragments-sdk/ui';
4
- import type { ReactNode } from 'react';
4
+ import { Fragment, useId, type ReactNode } from 'react';
5
5
  import { DocsSearchCommand } from './DocsSearchCommand';
6
6
  import type { DocsNavLinkRenderer, HeaderNavEntry, NavSection, SearchItem } from './types';
7
- import { isDropdown } from './types';
7
+ import { isDropdown, isMegaMenu } from './types';
8
+ import megaStyles from './DocsHeaderBar.module.scss';
8
9
 
9
10
  interface DocsHeaderBarProps {
10
11
  brand: ReactNode;
@@ -20,9 +21,9 @@ interface DocsHeaderBarProps {
20
21
  navAriaLabel?: string;
21
22
  }
22
23
 
23
- const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
24
- <a href={href} onClick={onClick}>
25
- {label}
24
+ const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick, className, children }) => (
25
+ <a href={href} onClick={onClick} className={className}>
26
+ {children ?? label}
26
27
  </a>
27
28
  );
28
29
 
@@ -30,6 +31,54 @@ function defaultIsActive(href: string, currentPath: string): boolean {
30
31
  return currentPath === href || currentPath.startsWith(`${href}/`);
31
32
  }
32
33
 
34
+ function PackageLogo({ abbr, gradient }: { abbr: string; gradient: [string, string] }) {
35
+ const gradientId = `pkg-grad-${abbr}-${useId().replace(/[:]/g, '')}`;
36
+ return (
37
+ <svg
38
+ className={megaStyles.packageLogo}
39
+ viewBox="0 0 36 36"
40
+ width={36}
41
+ height={36}
42
+ aria-hidden
43
+ >
44
+ <defs>
45
+ <linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
46
+ <stop offset="0%" stopColor={gradient[0]} />
47
+ <stop offset="100%" stopColor={gradient[1]} />
48
+ </linearGradient>
49
+ </defs>
50
+ <rect width="36" height="36" rx="2" fill={`url(#${gradientId})`} />
51
+ <path
52
+ d="M0 25 Q9 18 18 25 Q27 32 36 25 L36 36 L0 36 Z"
53
+ fill="white"
54
+ fillOpacity={0.15}
55
+ />
56
+ <path
57
+ d="M0 30 Q9 23 18 30 Q27 37 36 30 L36 36 L0 36 Z"
58
+ fill="white"
59
+ fillOpacity={0.1}
60
+ />
61
+ <text
62
+ x="18"
63
+ y="19"
64
+ textAnchor="middle"
65
+ dominantBaseline="central"
66
+ fill="white"
67
+ fontSize="12"
68
+ fontWeight="700"
69
+ fontFamily="'Geist Mono', 'SF Mono', SFMono-Regular, ui-monospace, monospace"
70
+ letterSpacing="-0.02em"
71
+ >
72
+ {abbr}
73
+ </text>
74
+ </svg>
75
+ );
76
+ }
77
+
78
+ function MegaMenuBadge({ text }: { text: string }) {
79
+ return <span className={megaStyles.megaMenuBadge}>{text}</span>;
80
+ }
81
+
33
82
  export function DocsHeaderBar({
34
83
  brand,
35
84
  headerNav,
@@ -51,7 +100,78 @@ export function DocsHeaderBar({
51
100
  <NavigationMenu aria-label={navAriaLabel}>
52
101
  <NavigationMenu.List>
53
102
  {headerNav.map((entry) =>
54
- isDropdown(entry) ? (
103
+ isMegaMenu(entry) ? (
104
+ <NavigationMenu.Item key={entry.label} value={entry.label}>
105
+ <NavigationMenu.Trigger>{entry.label}</NavigationMenu.Trigger>
106
+ <NavigationMenu.Content>
107
+ <div
108
+ className={megaStyles.megaMenuGrid}
109
+ data-columns={entry.sections.length}
110
+ style={{
111
+ gridTemplateColumns: entry.sections
112
+ .map((_, i) => {
113
+ const parts: string[] = [];
114
+ if (i > 0) parts.push('1px'); // divider column
115
+ parts.push('1fr');
116
+ return parts.join(' ');
117
+ })
118
+ .join(' '),
119
+ }}
120
+ >
121
+ {entry.sections.map((section, sectionIdx) => (
122
+ <Fragment key={section.title}>
123
+ {sectionIdx > 0 && <div className={megaStyles.megaMenuDivider} />}
124
+ <div className={megaStyles.megaMenuSection}>
125
+ <div className={megaStyles.megaMenuSectionTitle}>{section.title}</div>
126
+ {section.items.map((item) => {
127
+ const active = isActive(item.href, currentPath);
128
+ const className = [
129
+ megaStyles.megaMenuItem,
130
+ active ? megaStyles.megaMenuItemActive : '',
131
+ ]
132
+ .filter(Boolean)
133
+ .join(' ');
134
+
135
+ return (
136
+ <NavigationMenu.Link
137
+ key={item.href}
138
+ href={item.href}
139
+ active={active}
140
+ asChild
141
+ >
142
+ {renderLink({
143
+ href: item.href,
144
+ label: item.label,
145
+ className,
146
+ children: (
147
+ <>
148
+ {item.abbr && item.gradient ? (
149
+ <PackageLogo abbr={item.abbr} gradient={item.gradient} />
150
+ ) : null}
151
+ <span className={megaStyles.megaMenuItemBody}>
152
+ <span className={megaStyles.megaMenuItemTitle}>
153
+ {item.label}
154
+ {item.badge ? <MegaMenuBadge text={item.badge} /> : null}
155
+ </span>
156
+ {item.description ? (
157
+ <span className={megaStyles.megaMenuItemDesc}>
158
+ {item.description}
159
+ </span>
160
+ ) : null}
161
+ </span>
162
+ </>
163
+ ),
164
+ })}
165
+ </NavigationMenu.Link>
166
+ );
167
+ })}
168
+ </div>
169
+ </Fragment>
170
+ ))}
171
+ </div>
172
+ </NavigationMenu.Content>
173
+ </NavigationMenu.Item>
174
+ ) : isDropdown(entry) ? (
55
175
  <NavigationMenu.Item key={entry.label} value={entry.label}>
56
176
  <NavigationMenu.Trigger>{entry.label}</NavigationMenu.Trigger>
57
177
  <NavigationMenu.Content>
@@ -88,23 +208,38 @@ export function DocsHeaderBar({
88
208
  <NavigationMenu.MobileBrand>{brand}</NavigationMenu.MobileBrand>
89
209
 
90
210
  <NavigationMenu.MobileContent>
91
- {/* Render all headerNav items in the mobile drawer */}
211
+ {/* Top-level nav items (skip mega-menus they get their own sections below) */}
92
212
  <NavigationMenu.MobileSection>
93
- {headerNav.map((entry) =>
94
- isDropdown(entry) ? (
95
- entry.items.map((child) => (
213
+ {headerNav.flatMap((entry) => {
214
+ if (isMegaMenu(entry)) return [];
215
+ if (isDropdown(entry)) {
216
+ return entry.items.map((child) => (
96
217
  <NavigationMenu.Link key={child.href} href={child.href} asChild>
97
218
  {renderLink({ href: child.href, label: child.label })}
98
219
  </NavigationMenu.Link>
99
- ))
100
- ) : (
220
+ ));
221
+ }
222
+ return [(
101
223
  <NavigationMenu.Link key={entry.href} href={entry.href} asChild>
102
224
  {renderLink({ href: entry.href, label: entry.label })}
103
225
  </NavigationMenu.Link>
104
- )
105
- )}
226
+ )];
227
+ })}
106
228
  </NavigationMenu.MobileSection>
107
229
 
230
+ {/* Mega-menu sections with labels */}
231
+ {headerNav.filter(isMegaMenu).flatMap((entry) =>
232
+ entry.sections.map((section) => (
233
+ <NavigationMenu.MobileSection key={section.title} label={section.title}>
234
+ {section.items.map((item) => (
235
+ <NavigationMenu.Link key={item.href} href={item.href} asChild>
236
+ {renderLink({ href: item.href, label: item.label })}
237
+ </NavigationMenu.Link>
238
+ ))}
239
+ </NavigationMenu.MobileSection>
240
+ ))
241
+ )}
242
+
108
243
  {mobileSections.map((section) => (
109
244
  <NavigationMenu.MobileSection key={section.title} label={section.title}>
110
245
  {section.items.map((item) => (
@@ -18,9 +18,9 @@ function defaultIsActive(href: string, currentPath: string): boolean {
18
18
  return currentPath === href;
19
19
  }
20
20
 
21
- const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
22
- <a href={href} onClick={onClick}>
23
- {label}
21
+ const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick, className, children }) => (
22
+ <a href={href} onClick={onClick} className={className}>
23
+ {children ?? label}
24
24
  </a>
25
25
  );
26
26
 
@@ -22,8 +22,11 @@ export type {
22
22
  HeaderNavEntry,
23
23
  HeaderNavLink,
24
24
  HeaderNavDropdown,
25
+ MegaMenuItem,
26
+ MegaMenuSection,
27
+ HeaderNavMegaMenu,
25
28
  } from './types';
26
- export { isDropdown } from './types';
29
+ export { isDropdown, isMegaMenu } from './types';
27
30
 
28
31
  export { VariantPreviewCard } from './VariantPreviewCard';
29
32
  export type { VariantPreviewCardProps } from './VariantPreviewCard';
@@ -1,4 +1,4 @@
1
- import type { ReactElement } from 'react';
1
+ import type { ReactElement, ReactNode } from 'react';
2
2
 
3
3
  export interface NavItem {
4
4
  label: string;
@@ -20,6 +20,8 @@ export interface DocsNavLinkRenderProps {
20
20
  href: string;
21
21
  label: string;
22
22
  onClick?: () => void;
23
+ className?: string;
24
+ children?: ReactNode;
23
25
  }
24
26
 
25
27
  export type DocsNavLinkRenderer = (props: DocsNavLinkRenderProps) => ReactElement;
@@ -34,10 +36,34 @@ export interface HeaderNavDropdown {
34
36
  items: NavItem[];
35
37
  }
36
38
 
37
- export type HeaderNavEntry = HeaderNavLink | HeaderNavDropdown;
39
+ export interface MegaMenuItem extends NavItem {
40
+ description?: string;
41
+ /** Two-letter abbreviation for the package logo (e.g. "CL", "MC") */
42
+ abbr?: string;
43
+ /** Gradient color pair for the package logo [from, to] */
44
+ gradient?: [string, string];
45
+ /** Optional badge text (e.g. "Coming Soon") */
46
+ badge?: string;
47
+ }
48
+
49
+ export interface MegaMenuSection {
50
+ title: string;
51
+ items: MegaMenuItem[];
52
+ }
53
+
54
+ export interface HeaderNavMegaMenu {
55
+ label: string;
56
+ sections: MegaMenuSection[];
57
+ }
58
+
59
+ export type HeaderNavEntry = HeaderNavLink | HeaderNavDropdown | HeaderNavMegaMenu;
38
60
 
39
61
  export function isDropdown(entry: HeaderNavEntry): entry is HeaderNavDropdown {
40
- return 'items' in entry;
62
+ return 'items' in entry && !('sections' in entry);
63
+ }
64
+
65
+ export function isMegaMenu(entry: HeaderNavEntry): entry is HeaderNavMegaMenu {
66
+ return 'sections' in entry;
41
67
  }
42
68
 
43
69
  /**
@@ -218,7 +218,7 @@ import type {
218
218
  TokenFix,
219
219
  TokenUsageSummary,
220
220
  DesignToken,
221
- } from "../core/index.js";
221
+ } from "@fragments-sdk/core";
222
222
 
223
223
  /**
224
224
  * Enhanced style diff result with token information
@@ -93,7 +93,7 @@ export function createRuntimeWebMCPTools(options: RuntimeToolsOptions = {}): Web
93
93
 
94
94
  const entry: { name: string; value: string; computed?: string; category?: string } = {
95
95
  name: token.name,
96
- value: token.value,
96
+ value: String(token.value),
97
97
  category: cat,
98
98
  };
99
99