@fragments-sdk/ui 0.8.4 → 0.8.5

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 (30) hide show
  1. package/fragments.json +1 -1
  2. package/package.json +29 -7
  3. package/src/blocks/NavigationHeader.block.ts +89 -0
  4. package/src/components/Chart/Chart.fragment.tsx +3 -0
  5. package/src/components/Chart/index.tsx +62 -14
  6. package/src/components/CodeBlock/CodeBlock.fragment.tsx +3 -0
  7. package/src/components/CodeBlock/CodeBlock.test.tsx +6 -6
  8. package/src/components/CodeBlock/index.tsx +38 -3
  9. package/src/components/ColorPicker/ColorPicker.fragment.tsx +3 -0
  10. package/src/components/ColorPicker/index.tsx +24 -2
  11. package/src/components/DatePicker/DatePicker.fragment.tsx +4 -0
  12. package/src/components/DatePicker/index.tsx +101 -43
  13. package/src/components/Header/Header.fragment.tsx +37 -0
  14. package/src/components/Link/Link.fragment.tsx +17 -0
  15. package/src/components/Link/Link.test.tsx +23 -0
  16. package/src/components/Link/index.tsx +20 -0
  17. package/src/components/Markdown/Markdown.fragment.tsx +4 -0
  18. package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +270 -0
  19. package/src/components/NavigationMenu/NavigationMenu.module.scss +516 -0
  20. package/src/components/NavigationMenu/NavigationMenu.test.tsx +457 -0
  21. package/src/components/NavigationMenu/NavigationMenuContext.ts +89 -0
  22. package/src/components/NavigationMenu/index.tsx +854 -0
  23. package/src/components/NavigationMenu/useNavigationMenu.ts +91 -0
  24. package/src/components/Sidebar/Sidebar.fragment.tsx +1 -1
  25. package/src/components/Sidebar/Sidebar.module.scss +2 -1
  26. package/src/components/Table/Table.fragment.tsx +3 -0
  27. package/src/components/Table/index.tsx +70 -24
  28. package/src/index.ts +25 -0
  29. package/src/tokens/_palettes.scss +8 -7
  30. package/src/tokens/_seeds.scss +2 -2
@@ -1,17 +1,16 @@
1
1
  import * as React from 'react';
2
2
  import { Popover as BasePopover } from '@base-ui/react/popover';
3
- import { DayPicker, UI, SelectionState, DayFlag } from 'react-day-picker';
4
- import { format } from 'date-fns';
5
3
  import styles from './DatePicker.module.scss';
6
4
  // Import globals to ensure CSS variables are defined
7
5
  import '../../styles/globals.scss';
8
6
 
9
7
  // ============================================
10
- // Types
8
+ // Types (self-owned — no external dependency for types)
11
9
  // ============================================
12
10
 
13
- export type { DateRange, Matcher } from 'react-day-picker';
14
- import type { DateRange, Matcher, Locale } from 'react-day-picker';
11
+ export type DateRange = { from: Date | undefined; to?: Date | undefined };
12
+ export type Matcher = Date | DateRange | ((date: Date) => boolean) | Date[];
13
+ type Locale = { [key: string]: unknown };
15
14
 
16
15
  export interface DatePickerProps {
17
16
  children: React.ReactNode;
@@ -75,6 +74,39 @@ export interface DatePickerPresetProps {
75
74
  className?: string;
76
75
  }
77
76
 
77
+ // ============================================
78
+ // Lazy-loaded dependencies (react-day-picker + date-fns)
79
+ // ============================================
80
+
81
+ let _DayPicker: any = null;
82
+ let _dpUI: any = null;
83
+ let _SelectionState: any = null;
84
+ let _DayFlag: any = null;
85
+ let _formatDate: ((date: Date, fmt: string) => string) | null = null;
86
+ let _dpLoaded = false;
87
+ let _dpFailed = false;
88
+
89
+ function loadDatePickerDeps() {
90
+ if (_dpLoaded) return;
91
+ _dpLoaded = true;
92
+ try {
93
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
94
+ const rdp = require('react-day-picker');
95
+ _DayPicker = rdp.DayPicker;
96
+ _dpUI = rdp.UI;
97
+ _SelectionState = rdp.SelectionState;
98
+ _DayFlag = rdp.DayFlag;
99
+ } catch {
100
+ _dpFailed = true;
101
+ }
102
+ try {
103
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
104
+ _formatDate = require('date-fns').format;
105
+ } catch {
106
+ _dpFailed = true;
107
+ }
108
+ }
109
+
78
110
  // ============================================
79
111
  // Icons
80
112
  // ============================================
@@ -178,45 +210,53 @@ function useDatePickerContext() {
178
210
  // ============================================
179
211
 
180
212
  function defaultFormatDate(date: Date): string {
181
- return format(date, 'PPP');
213
+ if (_formatDate) return _formatDate(date, 'PPP');
214
+ return date.toLocaleDateString();
182
215
  }
183
216
 
184
217
  function defaultFormatRange(range: DateRange): string {
185
218
  if (!range.from) return '';
186
- if (!range.to) return format(range.from, 'LLL dd, y');
187
- return `${format(range.from, 'LLL dd, y')} - ${format(range.to, 'LLL dd, y')}`;
219
+ if (_formatDate) {
220
+ if (!range.to) return _formatDate(range.from, 'LLL dd, y');
221
+ return `${_formatDate(range.from, 'LLL dd, y')} - ${_formatDate(range.to, 'LLL dd, y')}`;
222
+ }
223
+ if (!range.to) return range.from.toLocaleDateString();
224
+ return `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`;
188
225
  }
189
226
 
190
227
  // ============================================
191
- // ClassNames mapping
228
+ // ClassNames mapping (built lazily)
192
229
  // ============================================
193
230
 
194
- const calendarClassNames = {
195
- [UI.Root]: styles.calendar,
196
- [UI.Months]: styles.months,
197
- [UI.Month]: styles.month,
198
- [UI.MonthCaption]: styles.monthCaption,
199
- [UI.CaptionLabel]: styles.captionLabel,
200
- [UI.Nav]: styles.nav,
201
- [UI.PreviousMonthButton]: styles.navButton,
202
- [UI.NextMonthButton]: styles.navButton,
203
- [UI.MonthGrid]: styles.monthGrid,
204
- [UI.Weekdays]: styles.weekdays,
205
- [UI.Weekday]: styles.weekday,
206
- [UI.Weeks]: styles.weeks,
207
- [UI.Week]: styles.week,
208
- [UI.Day]: styles.day,
209
- [UI.DayButton]: styles.dayButton,
210
- [UI.Chevron]: styles.chevron,
211
- [SelectionState.selected]: styles.selected,
212
- [SelectionState.range_start]: styles.rangeStart,
213
- [SelectionState.range_middle]: styles.rangeMiddle,
214
- [SelectionState.range_end]: styles.rangeEnd,
215
- [DayFlag.today]: styles.today,
216
- [DayFlag.outside]: styles.outside,
217
- [DayFlag.disabled]: styles.disabled,
218
- [DayFlag.focused]: styles.focused,
219
- };
231
+ function getCalendarClassNames() {
232
+ if (!_dpUI || !_SelectionState || !_DayFlag) return {};
233
+ return {
234
+ [_dpUI.Root]: styles.calendar,
235
+ [_dpUI.Months]: styles.months,
236
+ [_dpUI.Month]: styles.month,
237
+ [_dpUI.MonthCaption]: styles.monthCaption,
238
+ [_dpUI.CaptionLabel]: styles.captionLabel,
239
+ [_dpUI.Nav]: styles.nav,
240
+ [_dpUI.PreviousMonthButton]: styles.navButton,
241
+ [_dpUI.NextMonthButton]: styles.navButton,
242
+ [_dpUI.MonthGrid]: styles.monthGrid,
243
+ [_dpUI.Weekdays]: styles.weekdays,
244
+ [_dpUI.Weekday]: styles.weekday,
245
+ [_dpUI.Weeks]: styles.weeks,
246
+ [_dpUI.Week]: styles.week,
247
+ [_dpUI.Day]: styles.day,
248
+ [_dpUI.DayButton]: styles.dayButton,
249
+ [_dpUI.Chevron]: styles.chevron,
250
+ [_SelectionState.selected]: styles.selected,
251
+ [_SelectionState.range_start]: styles.rangeStart,
252
+ [_SelectionState.range_middle]: styles.rangeMiddle,
253
+ [_SelectionState.range_end]: styles.rangeEnd,
254
+ [_DayFlag.today]: styles.today,
255
+ [_DayFlag.outside]: styles.outside,
256
+ [_DayFlag.disabled]: styles.disabled,
257
+ [_DayFlag.focused]: styles.focused,
258
+ };
259
+ }
220
260
 
221
261
  // ============================================
222
262
  // Components
@@ -241,6 +281,8 @@ function DatePickerRoot({
241
281
  onOpenChange,
242
282
  name,
243
283
  }: DatePickerProps) {
284
+ // Load deps eagerly so date formatters are available in the trigger
285
+ loadDatePickerDeps();
244
286
  const [internalSelected, setInternalSelected] = React.useState<Date | null>(
245
287
  selectedProp ?? null
246
288
  );
@@ -439,13 +481,11 @@ function DatePickerContent({
439
481
  }
440
482
 
441
483
  function DatePickerCalendar({ numberOfMonths: numberOfMonthsProp, className }: DatePickerCalendarProps) {
484
+ loadDatePickerDeps();
485
+
442
486
  const ctx = useDatePickerContext();
443
487
  const monthCount = numberOfMonthsProp ?? ctx.numberOfMonths;
444
488
 
445
- const calendarClasses = className
446
- ? { ...calendarClassNames, [UI.Root]: [styles.calendar, className].join(' ') }
447
- : calendarClassNames;
448
-
449
489
  const components = React.useMemo(
450
490
  () => ({
451
491
  Chevron: (props: { orientation?: string }) =>
@@ -454,16 +494,34 @@ function DatePickerCalendar({ numberOfMonths: numberOfMonthsProp, className }: D
454
494
  []
455
495
  );
456
496
 
497
+ if (_dpFailed || !_DayPicker) {
498
+ if (_dpFailed && process.env.NODE_ENV === 'development') {
499
+ console.warn(
500
+ '[@fragments-sdk/ui] DatePicker: react-day-picker and date-fns are not installed. ' +
501
+ 'Install them with: npm install react-day-picker date-fns'
502
+ );
503
+ }
504
+ return null;
505
+ }
506
+
507
+ const calendarClassNames = getCalendarClassNames();
508
+
509
+ const calendarClasses = className
510
+ ? { ...calendarClassNames, [_dpUI.Root]: [styles.calendar, className].join(' ') }
511
+ : calendarClassNames;
512
+
513
+ const DayPickerComponent = _DayPicker;
514
+
457
515
  if (ctx.mode === 'range') {
458
516
  const rangeSelected = ctx.selectedRange
459
517
  ? { from: ctx.selectedRange.from ?? undefined, to: ctx.selectedRange.to ?? undefined }
460
518
  : undefined;
461
519
 
462
520
  return (
463
- <DayPicker
521
+ <DayPickerComponent
464
522
  mode="range"
465
523
  selected={rangeSelected}
466
- onSelect={(range) => {
524
+ onSelect={(range: any) => {
467
525
  ctx.setSelectedRange(range ? { from: range.from ?? undefined, to: range.to ?? undefined } : null);
468
526
  }}
469
527
  numberOfMonths={monthCount}
@@ -478,10 +536,10 @@ function DatePickerCalendar({ numberOfMonths: numberOfMonthsProp, className }: D
478
536
  }
479
537
 
480
538
  return (
481
- <DayPicker
539
+ <DayPickerComponent
482
540
  mode="single"
483
541
  selected={ctx.selected ?? undefined}
484
- onSelect={(date) => {
542
+ onSelect={(date: any) => {
485
543
  ctx.setSelected(date ?? null);
486
544
  }}
487
545
  numberOfMonths={monthCount}
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { defineFragment } from '@fragments/core';
3
3
  import { Header } from '.';
4
+ import { NavigationMenu } from '../NavigationMenu';
4
5
  import { ThemeToggle, ThemeProvider } from '../Theme';
5
6
  import { Button } from '../Button';
6
7
  import { Input } from '../Input';
@@ -40,6 +41,8 @@ export default defineFragment({
40
41
  'Use Header.Spacer to push items apart',
41
42
  'Use Header.NavMenu to group related nav items under a dropdown',
42
43
  'Use Header.NavMenuItem inside Header.NavMenu for dropdown items',
44
+ 'For rich dropdown content (titles, descriptions, icons), use NavigationMenu instead of Header.Nav',
45
+ 'NavigationMenu also provides an automatic mobile drawer, replacing Header.Trigger + Sidebar for mobile nav',
43
46
  ],
44
47
  accessibility: [
45
48
  'Include Header.SkipLink for keyboard users',
@@ -73,6 +76,7 @@ export default defineFragment({
73
76
  { component: 'AppShell', relationship: 'parent', note: 'Header is typically used inside AppShell.Header' },
74
77
  { component: 'Sidebar', relationship: 'sibling', note: 'Header.Trigger toggles Sidebar on mobile' },
75
78
  { component: 'Theme', relationship: 'child', note: 'ThemeToggle is commonly placed in Header.Actions' },
79
+ { component: 'NavigationMenu', relationship: 'child', note: 'Use NavigationMenu inside Header for rich dropdown nav with auto mobile drawer' },
76
80
  ],
77
81
 
78
82
  ai: {
@@ -211,5 +215,38 @@ export default defineFragment({
211
215
  </Header>
212
216
  ),
213
217
  },
218
+ {
219
+ name: 'With NavigationMenu',
220
+ description: 'Header using NavigationMenu for rich dropdown content panels with titles, descriptions, and automatic mobile drawer.',
221
+ render: () => (
222
+ <ThemeProvider defaultMode="light">
223
+ <Header>
224
+ <Header.Brand href="/">MyApp</Header.Brand>
225
+ <NavigationMenu aria-label="Site navigation">
226
+ <NavigationMenu.List>
227
+ <NavigationMenu.Item value="products">
228
+ <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
229
+ <NavigationMenu.Content>
230
+ <div style={{ display: 'flex', flexDirection: 'column', padding: '4px', minWidth: '200px' }}>
231
+ <NavigationMenu.Link href="/analytics" title="Analytics" description="Track your metrics." />
232
+ <NavigationMenu.Link href="/automation" title="Automation" description="Automate workflows." />
233
+ </div>
234
+ </NavigationMenu.Content>
235
+ </NavigationMenu.Item>
236
+ <NavigationMenu.Item>
237
+ <NavigationMenu.Link href="/pricing">Pricing</NavigationMenu.Link>
238
+ </NavigationMenu.Item>
239
+ </NavigationMenu.List>
240
+ <NavigationMenu.Viewport />
241
+ </NavigationMenu>
242
+ <Header.Spacer />
243
+ <Header.Actions>
244
+ <ThemeToggle size="md" />
245
+ <Button variant="primary" size="sm">Sign Up</Button>
246
+ </Header.Actions>
247
+ </Header>
248
+ </ThemeProvider>
249
+ ),
250
+ },
214
251
  ],
215
252
  });
@@ -64,6 +64,11 @@ export default defineFragment({
64
64
  description: 'Opens in new tab with noopener noreferrer',
65
65
  default: 'false',
66
66
  },
67
+ asChild: {
68
+ type: 'boolean',
69
+ description: 'Render as child element (polymorphic). Merges link props onto the single child. Useful for rendering as Next.js Link for client-side navigation.',
70
+ default: 'false',
71
+ },
67
72
  },
68
73
 
69
74
  relations: [
@@ -78,6 +83,7 @@ export default defineFragment({
78
83
  'variant: default|subtle|muted - visual style',
79
84
  'underline: always|hover|none - underline behavior',
80
85
  'external: boolean - opens in new tab',
86
+ 'asChild: boolean - render as child element for polymorphic usage (e.g. Next.js Link)',
81
87
  ],
82
88
  scenarioTags: [
83
89
  'navigation.link',
@@ -124,5 +130,16 @@ export default defineFragment({
124
130
  </Link>
125
131
  ),
126
132
  },
133
+ {
134
+ name: 'As Child (Polymorphic)',
135
+ description: 'Renders as a custom element while applying Link styles. Useful with Next.js Link for client-side navigation.',
136
+ render: () => (
137
+ <Link asChild variant="subtle">
138
+ <button type="button" onClick={() => alert('Navigated!')}>
139
+ Polymorphic link as button
140
+ </button>
141
+ </Link>
142
+ ),
143
+ },
127
144
  ],
128
145
  });
@@ -30,6 +30,29 @@ describe('Link', () => {
30
30
  expect(ref).toHaveBeenCalled();
31
31
  });
32
32
 
33
+ it('renders as child element when asChild is true', () => {
34
+ render(
35
+ <Link asChild variant="subtle">
36
+ <button type="button">Click me</button>
37
+ </Link>
38
+ );
39
+ const btn = screen.getByRole('button', { name: 'Click me' });
40
+ expect(btn.tagName).toBe('BUTTON');
41
+ expect(btn).toHaveClass('link');
42
+ expect(btn).toHaveClass('subtle');
43
+ });
44
+
45
+ it('merges classNames when asChild is true', () => {
46
+ render(
47
+ <Link asChild variant="default">
48
+ <a href="/test" className="custom-class">Test</a>
49
+ </Link>
50
+ );
51
+ const link = screen.getByRole('link', { name: 'Test' });
52
+ expect(link).toHaveClass('link');
53
+ expect(link).toHaveClass('custom-class');
54
+ });
55
+
33
56
  it('has no accessibility violations', async () => {
34
57
  const { container } = render(<Link href="/page">Accessible link</Link>);
35
58
  await expectNoA11yViolations(container);
@@ -10,6 +10,11 @@ export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>
10
10
  underline?: 'always' | 'hover' | 'none';
11
11
  /** Open in new tab (adds rel="noopener noreferrer") */
12
12
  external?: boolean;
13
+ /**
14
+ * Render as child element (polymorphic). When true, clones the single child
15
+ * and merges link props onto it. Useful for rendering as Next.js Link, etc.
16
+ */
17
+ asChild?: boolean;
13
18
  /** Additional class name */
14
19
  className?: string;
15
20
  /** Inline styles */
@@ -23,6 +28,7 @@ const LinkRoot = React.forwardRef<HTMLAnchorElement, LinkProps>(
23
28
  variant = 'default',
24
29
  underline = 'hover',
25
30
  external = false,
31
+ asChild = false,
26
32
  className,
27
33
  style,
28
34
  target,
@@ -48,6 +54,20 @@ const LinkRoot = React.forwardRef<HTMLAnchorElement, LinkProps>(
48
54
  }
49
55
  : { target, rel };
50
56
 
57
+ if (asChild && React.isValidElement(children)) {
58
+ const childProps = children.props as Record<string, unknown>;
59
+ const mergedProps: Record<string, unknown> = {
60
+ ref,
61
+ className: childProps.className
62
+ ? `${classes} ${childProps.className}`
63
+ : classes,
64
+ style: { ...style, ...(childProps.style as React.CSSProperties | undefined) },
65
+ ...externalProps,
66
+ ...props,
67
+ };
68
+ return React.cloneElement(children, mergedProps);
69
+ }
70
+
51
71
  return (
52
72
  <a ref={ref} className={classes} style={style} {...externalProps} {...props}>
53
73
  {children}
@@ -85,6 +85,10 @@ export default defineFragment({
85
85
  status: 'stable',
86
86
  tags: ['markdown', 'prose', 'content', 'text', 'ai', 'chat'],
87
87
  since: '0.7.0',
88
+ dependencies: [
89
+ { name: 'react-markdown', version: '>=9.0.0', reason: 'Markdown parsing and rendering' },
90
+ { name: 'remark-gfm', version: '>=4.0.0', reason: 'GitHub Flavored Markdown support (optional)' },
91
+ ],
88
92
  },
89
93
 
90
94
  usage: {
@@ -0,0 +1,270 @@
1
+ import React from 'react';
2
+ import { defineFragment } from '@fragments/core';
3
+ import { NavigationMenu } from '.';
4
+
5
+ export default defineFragment({
6
+ component: NavigationMenu,
7
+
8
+ meta: {
9
+ name: 'NavigationMenu',
10
+ description: 'Rich navigation menu for site headers with dropdown content panels, animated viewport transitions, and automatic mobile drawer. Supports structured links with titles, descriptions, and icons.',
11
+ category: 'navigation',
12
+ status: 'stable',
13
+ tags: ['navigation', 'menu', 'header', 'dropdown', 'navbar', 'mobile', 'responsive', 'drawer'],
14
+ since: '0.9.0',
15
+ },
16
+
17
+ usage: {
18
+ when: [
19
+ 'Site-level header navigation with rich dropdown content',
20
+ 'Navigation with titles, descriptions, and icons in dropdowns',
21
+ 'Responsive navigation that converts to a mobile drawer automatically',
22
+ 'Multi-section navigation requiring animated viewport transitions',
23
+ ],
24
+ whenNot: [
25
+ 'Simple flat navigation without dropdowns (use Header.Nav)',
26
+ 'Application menus with actions like cut/paste (use Menu)',
27
+ 'Sidebar navigation (use Sidebar)',
28
+ 'Breadcrumb trail navigation (use Breadcrumbs)',
29
+ ],
30
+ guidelines: [
31
+ 'Place inside Header component for site-level navigation',
32
+ 'Use NavigationMenu.Viewport for animated content panel transitions',
33
+ 'Use NavigationMenu.MobileContent to add extra sections to the mobile drawer',
34
+ 'Use structured links (title + description) for rich dropdown content',
35
+ 'Use simple NavigationMenu.Link for items without dropdown content',
36
+ 'Triggers open on hover (desktop) with configurable delay',
37
+ ],
38
+ accessibility: [
39
+ 'Uses disclosure pattern (not menu role) per W3C guidance',
40
+ 'Keyboard: Arrow keys navigate between triggers, Enter/Space toggles, Escape closes',
41
+ 'Content panels have role="region" with aria-labelledby pointing to trigger',
42
+ 'Mobile drawer has focus trap, Escape to close, and aria-modal',
43
+ 'Supports prefers-reduced-motion for all animations',
44
+ ],
45
+ },
46
+
47
+ props: {
48
+ children: {
49
+ type: 'node',
50
+ description: 'NavigationMenu.List, NavigationMenu.Viewport, and optionally NavigationMenu.MobileContent',
51
+ required: true,
52
+ },
53
+ value: {
54
+ type: 'string',
55
+ description: 'Controlled open item value',
56
+ },
57
+ defaultValue: {
58
+ type: 'string',
59
+ description: 'Default open item value',
60
+ default: "''",
61
+ },
62
+ onValueChange: {
63
+ type: 'function',
64
+ description: 'Callback when open item changes',
65
+ },
66
+ orientation: {
67
+ type: 'string',
68
+ description: "'horizontal' | 'vertical'",
69
+ default: "'horizontal'",
70
+ },
71
+ delayDuration: {
72
+ type: 'number',
73
+ description: 'Hover delay before opening (ms)',
74
+ default: '200',
75
+ },
76
+ skipDelayDuration: {
77
+ type: 'number',
78
+ description: 'Duration after close during which moving to another trigger opens instantly (ms)',
79
+ default: '300',
80
+ },
81
+ },
82
+
83
+ relations: [
84
+ { component: 'Header', relationship: 'sibling', note: 'Place NavigationMenu inside Header for site navigation' },
85
+ { component: 'Sidebar', relationship: 'alternative', note: 'Use Sidebar for persistent side navigation, NavigationMenu for header dropdowns' },
86
+ { component: 'Menu', relationship: 'alternative', note: 'Use Menu for action menus (role=menu), NavigationMenu for site navigation (disclosure pattern)' },
87
+ { component: 'Breadcrumbs', relationship: 'sibling', note: 'Use Breadcrumbs for hierarchical page trail, NavigationMenu for site-level navigation' },
88
+ ],
89
+
90
+ contract: {
91
+ propsSummary: [
92
+ 'value: string — controlled open item',
93
+ 'onValueChange: (value) => void — open state handler',
94
+ 'orientation: horizontal | vertical — layout direction',
95
+ 'delayDuration: number — hover open delay (default: 200ms)',
96
+ 'NavigationMenu.Link: title + description + icon for structured links, or children for simple links',
97
+ 'NavigationMenu.MobileContent: slot for extra mobile-only sections',
98
+ ],
99
+ scenarioTags: [
100
+ 'navigation.site',
101
+ 'navigation.dropdown',
102
+ 'navigation.mobile',
103
+ 'layout.header',
104
+ ],
105
+ a11yRules: ['A11Y_DISCLOSURE_PATTERN', 'A11Y_KEYBOARD_NAV', 'A11Y_FOCUS_TRAP'],
106
+ },
107
+
108
+ ai: {
109
+ compositionPattern: 'compound',
110
+ subComponents: ['List', 'Item', 'Trigger', 'Content', 'Link', 'Indicator', 'Viewport', 'MobileContent', 'MobileSection'],
111
+ requiredChildren: ['List'],
112
+ commonPatterns: [
113
+ '<NavigationMenu><NavigationMenu.List><NavigationMenu.Item value="docs"><NavigationMenu.Trigger>Docs</NavigationMenu.Trigger><NavigationMenu.Content><NavigationMenu.Link href="/guides" title="Guides" description="Learn the basics" /></NavigationMenu.Content></NavigationMenu.Item></NavigationMenu.List><NavigationMenu.Viewport /></NavigationMenu>',
114
+ ],
115
+ },
116
+
117
+ variants: [
118
+ {
119
+ name: 'Default',
120
+ description: 'Two trigger items with dropdown content and one direct link',
121
+ render: () => (
122
+ <NavigationMenu>
123
+ <NavigationMenu.List>
124
+ <NavigationMenu.Item value="products">
125
+ <NavigationMenu.Trigger>Products</NavigationMenu.Trigger>
126
+ <NavigationMenu.Content>
127
+ <NavigationMenu.Link href="/analytics" title="Analytics" description="Track your metrics and KPIs." />
128
+ <NavigationMenu.Link href="/automation" title="Automation" description="Automate your workflows." />
129
+ </NavigationMenu.Content>
130
+ </NavigationMenu.Item>
131
+ <NavigationMenu.Item value="resources">
132
+ <NavigationMenu.Trigger>Resources</NavigationMenu.Trigger>
133
+ <NavigationMenu.Content>
134
+ <NavigationMenu.Link href="/docs" title="Documentation" description="Comprehensive API reference." />
135
+ <NavigationMenu.Link href="/blog" title="Blog" description="Latest news and updates." />
136
+ </NavigationMenu.Content>
137
+ </NavigationMenu.Item>
138
+ <NavigationMenu.Item>
139
+ <NavigationMenu.Link href="/pricing">Pricing</NavigationMenu.Link>
140
+ </NavigationMenu.Item>
141
+ </NavigationMenu.List>
142
+ <NavigationMenu.Viewport />
143
+ </NavigationMenu>
144
+ ),
145
+ },
146
+ {
147
+ name: 'With Rich Content',
148
+ description: 'Grid layout with icons, titles, and descriptions',
149
+ render: () => (
150
+ <NavigationMenu>
151
+ <NavigationMenu.List>
152
+ <NavigationMenu.Item value="platform">
153
+ <NavigationMenu.Trigger>Platform</NavigationMenu.Trigger>
154
+ <NavigationMenu.Content>
155
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', padding: '8px', minWidth: '400px' }}>
156
+ <NavigationMenu.Link
157
+ href="/dashboard"
158
+ title="Dashboard"
159
+ description="Overview of your project metrics."
160
+ />
161
+ <NavigationMenu.Link
162
+ href="/analytics"
163
+ title="Analytics"
164
+ description="Deep dive into your data."
165
+ />
166
+ <NavigationMenu.Link
167
+ href="/reports"
168
+ title="Reports"
169
+ description="Generate custom reports."
170
+ />
171
+ <NavigationMenu.Link
172
+ href="/settings"
173
+ title="Settings"
174
+ description="Configure your workspace."
175
+ />
176
+ </div>
177
+ </NavigationMenu.Content>
178
+ </NavigationMenu.Item>
179
+ </NavigationMenu.List>
180
+ <NavigationMenu.Viewport />
181
+ </NavigationMenu>
182
+ ),
183
+ },
184
+ {
185
+ name: 'With Simple Links',
186
+ description: 'Mix of triggers with simple link lists and plain links',
187
+ render: () => (
188
+ <NavigationMenu>
189
+ <NavigationMenu.List>
190
+ <NavigationMenu.Item value="company">
191
+ <NavigationMenu.Trigger>Company</NavigationMenu.Trigger>
192
+ <NavigationMenu.Content>
193
+ <div style={{ display: 'flex', flexDirection: 'column', padding: '4px', minWidth: '160px' }}>
194
+ <NavigationMenu.Link href="/about">About</NavigationMenu.Link>
195
+ <NavigationMenu.Link href="/careers">Careers</NavigationMenu.Link>
196
+ <NavigationMenu.Link href="/contact">Contact</NavigationMenu.Link>
197
+ </div>
198
+ </NavigationMenu.Content>
199
+ </NavigationMenu.Item>
200
+ <NavigationMenu.Item>
201
+ <NavigationMenu.Link href="/blog">Blog</NavigationMenu.Link>
202
+ </NavigationMenu.Item>
203
+ <NavigationMenu.Item>
204
+ <NavigationMenu.Link href="/docs">Docs</NavigationMenu.Link>
205
+ </NavigationMenu.Item>
206
+ </NavigationMenu.List>
207
+ <NavigationMenu.Viewport />
208
+ </NavigationMenu>
209
+ ),
210
+ },
211
+ {
212
+ name: 'With Featured Card',
213
+ description: 'Highlighted featured item alongside regular links',
214
+ render: () => (
215
+ <NavigationMenu>
216
+ <NavigationMenu.List>
217
+ <NavigationMenu.Item value="learn">
218
+ <NavigationMenu.Trigger>Learn</NavigationMenu.Trigger>
219
+ <NavigationMenu.Content>
220
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', padding: '8px', minWidth: '420px' }}>
221
+ <NavigationMenu.Link
222
+ href="/quickstart"
223
+ title="Quick Start"
224
+ description="Get up and running in 5 minutes with our getting started guide."
225
+ featured
226
+ />
227
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
228
+ <NavigationMenu.Link href="/docs" title="Documentation" description="API reference and guides." />
229
+ <NavigationMenu.Link href="/examples" title="Examples" description="Browse example projects." />
230
+ </div>
231
+ </div>
232
+ </NavigationMenu.Content>
233
+ </NavigationMenu.Item>
234
+ </NavigationMenu.List>
235
+ <NavigationMenu.Viewport />
236
+ </NavigationMenu>
237
+ ),
238
+ },
239
+ {
240
+ name: 'Vertical',
241
+ description: 'Vertical orientation with content panels to the right',
242
+ render: () => (
243
+ <div style={{ display: 'flex' }}>
244
+ <NavigationMenu orientation="vertical">
245
+ <NavigationMenu.List>
246
+ <NavigationMenu.Item value="overview">
247
+ <NavigationMenu.Trigger>Overview</NavigationMenu.Trigger>
248
+ <NavigationMenu.Content>
249
+ <div style={{ padding: '8px', minWidth: '200px' }}>
250
+ <NavigationMenu.Link href="/intro" title="Introduction" description="Learn the basics." />
251
+ </div>
252
+ </NavigationMenu.Content>
253
+ </NavigationMenu.Item>
254
+ <NavigationMenu.Item value="guides">
255
+ <NavigationMenu.Trigger>Guides</NavigationMenu.Trigger>
256
+ <NavigationMenu.Content>
257
+ <div style={{ padding: '8px', minWidth: '200px' }}>
258
+ <NavigationMenu.Link href="/install" title="Installation" description="Get started quickly." />
259
+ <NavigationMenu.Link href="/config" title="Configuration" description="Customize your setup." />
260
+ </div>
261
+ </NavigationMenu.Content>
262
+ </NavigationMenu.Item>
263
+ </NavigationMenu.List>
264
+ <NavigationMenu.Viewport />
265
+ </NavigationMenu>
266
+ </div>
267
+ ),
268
+ },
269
+ ],
270
+ });