@fragments-sdk/ui 0.7.5 → 0.8.0

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 (100) hide show
  1. package/README.md +58 -25
  2. package/fragments.json +1 -1
  3. package/package.json +15 -5
  4. package/src/blocks/AppShell.block.ts +2 -2
  5. package/src/blocks/InsetDashboardLayout.block.ts +1 -1
  6. package/src/blocks/LoginForm.block.ts +14 -7
  7. package/src/components/Accordion/Accordion.fragment.tsx +8 -2
  8. package/src/components/Alert/Alert.module.scss +4 -4
  9. package/src/components/AppShell/AppShell.fragment.tsx +1 -1
  10. package/src/components/AppShell/index.tsx +2 -0
  11. package/src/components/Avatar/Avatar.fragment.tsx +5 -1
  12. package/src/components/Avatar/Avatar.module.scss +1 -1
  13. package/src/components/Avatar/index.tsx +37 -1
  14. package/src/components/Badge/Badge.fragment.tsx +3 -3
  15. package/src/components/Badge/Badge.module.scss +4 -4
  16. package/src/components/Badge/index.tsx +5 -1
  17. package/src/components/Box/index.tsx +5 -1
  18. package/src/components/Button/Button.fragment.tsx +17 -16
  19. package/src/components/Button/index.tsx +5 -1
  20. package/src/components/ButtonGroup/index.tsx +5 -1
  21. package/src/components/Card/Card.fragment.tsx +5 -5
  22. package/src/components/Chart/Chart.fragment.tsx +9 -1
  23. package/src/components/Chart/index.tsx +22 -4
  24. package/src/components/Checkbox/index.tsx +5 -1
  25. package/src/components/Chip/Chip.fragment.tsx +0 -5
  26. package/src/components/Chip/Chip.module.scss +2 -2
  27. package/src/components/CodeBlock/CodeBlock.fragment.tsx +9 -3
  28. package/src/components/CodeBlock/CodeBlock.module.scss +1 -1
  29. package/src/components/ColorPicker/index.tsx +5 -1
  30. package/src/components/Combobox/Combobox.fragment.tsx +15 -7
  31. package/src/components/ConversationList/ConversationList.fragment.tsx +3 -3
  32. package/src/components/ConversationList/ConversationList.module.scss +1 -1
  33. package/src/components/DatePicker/DatePicker.fragment.tsx +245 -0
  34. package/src/components/DatePicker/DatePicker.module.scss +394 -0
  35. package/src/components/DatePicker/DatePicker.test.tsx +264 -0
  36. package/src/components/DatePicker/index.tsx +535 -0
  37. package/src/components/Field/Field.fragment.tsx +5 -4
  38. package/src/components/Fieldset/Fieldset.fragment.tsx +5 -4
  39. package/src/components/Form/Form.fragment.tsx +9 -3
  40. package/src/components/Form/index.tsx +5 -1
  41. package/src/components/Grid/Grid.fragment.tsx +4 -0
  42. package/src/components/Header/Header.fragment.tsx +36 -13
  43. package/src/components/Header/Header.module.scss +114 -1
  44. package/src/components/Header/Header.test.tsx +106 -1
  45. package/src/components/Header/index.tsx +100 -31
  46. package/src/components/Icon/Icon.fragment.tsx +6 -1
  47. package/src/components/Icon/index.tsx +5 -1
  48. package/src/components/Image/Image.fragment.tsx +2 -2
  49. package/src/components/Image/index.tsx +5 -1
  50. package/src/components/Input/Input.fragment.tsx +21 -3
  51. package/src/components/Input/Input.module.scss +1 -1
  52. package/src/components/Input/index.tsx +5 -1
  53. package/src/components/Link/Link.fragment.tsx +0 -4
  54. package/src/components/Link/index.tsx +5 -1
  55. package/src/components/Listbox/Listbox.fragment.tsx +0 -12
  56. package/src/components/Markdown/Markdown.module.scss +6 -3
  57. package/src/components/Markdown/index.tsx +5 -1
  58. package/src/components/Message/Message.fragment.tsx +8 -6
  59. package/src/components/Message/Message.module.scss +1 -1
  60. package/src/components/Progress/Progress.fragment.tsx +14 -0
  61. package/src/components/Progress/index.tsx +9 -2
  62. package/src/components/Prompt/Prompt.fragment.tsx +11 -0
  63. package/src/components/RadioGroup/RadioGroup.fragment.tsx +5 -0
  64. package/src/components/ScrollArea/ScrollArea.fragment.tsx +185 -0
  65. package/src/components/ScrollArea/ScrollArea.module.scss +136 -0
  66. package/src/components/ScrollArea/ScrollArea.test.tsx +38 -0
  67. package/src/components/ScrollArea/index.tsx +121 -0
  68. package/src/components/Select/Select.fragment.tsx +13 -5
  69. package/src/components/Separator/index.tsx +5 -1
  70. package/src/components/Sidebar/Sidebar.fragment.tsx +64 -11
  71. package/src/components/Sidebar/Sidebar.module.scss +68 -16
  72. package/src/components/Sidebar/Sidebar.test.tsx +31 -2
  73. package/src/components/Sidebar/index.tsx +69 -45
  74. package/src/components/Skeleton/Skeleton.fragment.tsx +5 -0
  75. package/src/components/Slider/index.tsx +5 -1
  76. package/src/components/Stack/Stack.fragment.tsx +2 -2
  77. package/src/components/Stack/index.tsx +5 -1
  78. package/src/components/Table/Table.fragment.tsx +29 -0
  79. package/src/components/Table/index.tsx +6 -1
  80. package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
  81. package/src/components/TableOfContents/TableOfContents.module.scss +71 -0
  82. package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
  83. package/src/components/TableOfContents/index.tsx +105 -0
  84. package/src/components/Text/index.tsx +5 -1
  85. package/src/components/Textarea/Textarea.fragment.tsx +8 -0
  86. package/src/components/Textarea/index.tsx +5 -1
  87. package/src/components/Theme/index.tsx +7 -0
  88. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +3 -2
  89. package/src/components/Toast/Toast.fragment.tsx +12 -0
  90. package/src/components/Toggle/index.tsx +5 -1
  91. package/src/components/Tooltip/Tooltip.fragment.tsx +18 -0
  92. package/src/components/Tooltip/index.tsx +6 -1
  93. package/src/components/VisuallyHidden/index.tsx +5 -1
  94. package/src/components/compound-pattern.test.ts +40 -0
  95. package/src/index.ts +29 -0
  96. package/src/recipes/AppShell.recipe.ts +2 -2
  97. package/src/recipes/LoginForm.recipe.ts +14 -7
  98. package/src/tokens/_computed.scss +12 -0
  99. package/src/tokens/_derive.scss +71 -0
  100. package/src/tokens/_variables.scss +22 -0
@@ -4,24 +4,17 @@ import { Header } from '.';
4
4
  import { ThemeToggle, ThemeProvider } from '../Theme';
5
5
  import { Button } from '../Button';
6
6
  import { Input } from '../Input';
7
-
8
- function SearchIcon() {
9
- return (
10
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor">
11
- <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z" />
12
- </svg>
13
- );
14
- }
7
+ import { MagnifyingGlass } from '@phosphor-icons/react';
15
8
 
16
9
  export default defineSegment({
17
10
  component: Header,
18
11
 
19
12
  meta: {
20
13
  name: 'Header',
21
- description: 'Composable header with slots for brand, navigation, search, and actions. Designed for use within AppShell with responsive mobile support.',
14
+ description: 'Composable header with slots for brand, navigation, search, and actions. Supports dropdown nav groups via Header.NavMenu. Designed for use within AppShell with responsive mobile support.',
22
15
  category: 'navigation',
23
16
  status: 'stable',
24
- tags: ['header', 'navigation', 'navbar', 'brand', 'layout'],
17
+ tags: ['header', 'navigation', 'navbar', 'brand', 'layout', 'dropdown'],
25
18
  since: '0.5.0',
26
19
  },
27
20
 
@@ -31,6 +24,7 @@ export default defineSegment({
31
24
  'Navigation bar with branding (stacked layout)',
32
25
  'Search and actions bar (sidebar-inset layout)',
33
26
  'Header with responsive mobile menu trigger',
27
+ 'Grouping related nav items under a dropdown menu',
34
28
  ],
35
29
  whenNot: [
36
30
  'Simple page titles (use heading elements)',
@@ -44,12 +38,15 @@ export default defineSegment({
44
38
  'Header.Trigger integrates with SidebarProvider for mobile menus',
45
39
  'Header.Nav is hidden on mobile; use sidebar for mobile navigation',
46
40
  'Use Header.Spacer to push items apart',
41
+ 'Use Header.NavMenu to group related nav items under a dropdown',
42
+ 'Use Header.NavMenuItem inside Header.NavMenu for dropdown items',
47
43
  ],
48
44
  accessibility: [
49
45
  'Include Header.SkipLink for keyboard users',
50
46
  'Navigation has aria-label for screen readers',
51
47
  'Active nav items use aria-current="page"',
52
48
  'Mobile trigger has aria-expanded state',
49
+ 'NavMenu dropdown opens with click and is keyboard navigable',
53
50
  ],
54
51
  },
55
52
 
@@ -75,14 +72,15 @@ export default defineSegment({
75
72
  relations: [
76
73
  { component: 'AppShell', relationship: 'parent', note: 'Header is typically used inside AppShell.Header' },
77
74
  { component: 'Sidebar', relationship: 'sibling', note: 'Header.Trigger toggles Sidebar on mobile' },
78
- { component: 'ThemeToggle', relationship: 'child', note: 'ThemeToggle is commonly placed in Header.Actions' },
75
+ { component: 'Theme', relationship: 'child', note: 'ThemeToggle is commonly placed in Header.Actions' },
79
76
  ],
80
77
 
81
78
  ai: {
82
79
  compositionPattern: 'compound',
83
- subComponents: ['SkipLink', 'Trigger', 'Brand', 'Nav', 'NavItem', 'Search', 'Spacer', 'Actions'],
80
+ subComponents: ['SkipLink', 'Trigger', 'Brand', 'Nav', 'NavItem', 'NavMenu', 'NavMenuItem', 'Search', 'Spacer', 'Actions'],
84
81
  commonPatterns: [
85
82
  '<Header><Header.Brand href="/">{appName}</Header.Brand><Header.Nav><Header.NavItem href="/home" active>Home</Header.NavItem></Header.Nav><Header.Spacer /><Header.Actions>{actions}</Header.Actions></Header>',
83
+ '<Header><Header.Nav><Header.NavItem href="/home">Home</Header.NavItem><Header.NavMenu label="Docs" active><Header.NavMenuItem href="/cli">CLI</Header.NavMenuItem><Header.NavMenuItem href="/mcp">MCP</Header.NavMenuItem></Header.NavMenu></Header.Nav></Header>',
86
84
  ],
87
85
  },
88
86
 
@@ -110,6 +108,31 @@ export default defineSegment({
110
108
  </ThemeProvider>
111
109
  ),
112
110
  },
111
+ {
112
+ name: 'With Dropdown Nav',
113
+ description: 'Header with a dropdown menu grouping related navigation links.',
114
+ render: () => (
115
+ <ThemeProvider defaultMode="light">
116
+ <Header>
117
+ <Header.Brand href="/">MyApp</Header.Brand>
118
+ <Header.Nav>
119
+ <Header.NavItem href="/components" active>Components</Header.NavItem>
120
+ <Header.NavItem href="/blocks">Blocks</Header.NavItem>
121
+ <Header.NavMenu label="Docs">
122
+ <Header.NavMenuItem href="/getting-started">Getting Started</Header.NavMenuItem>
123
+ <Header.NavMenuItem href="/cli">CLI Reference</Header.NavMenuItem>
124
+ <Header.NavMenuItem href="/mcp">MCP Tools</Header.NavMenuItem>
125
+ </Header.NavMenu>
126
+ <Header.NavItem href="/blog">Blog</Header.NavItem>
127
+ </Header.Nav>
128
+ <Header.Spacer />
129
+ <Header.Actions>
130
+ <ThemeToggle size="md" />
131
+ </Header.Actions>
132
+ </Header>
133
+ </ThemeProvider>
134
+ ),
135
+ },
113
136
  {
114
137
  name: 'For Sidebar Inset Layout',
115
138
  description: 'Header without brand (logo is in sidebar). Use with AppShell layout="sidebar-inset".',
@@ -163,7 +186,7 @@ export default defineSegment({
163
186
  fontSize: '14px',
164
187
  width: '280px'
165
188
  }}>
166
- <SearchIcon /> Search documentation...
189
+ <MagnifyingGlass size={16} /> Search documentation...
167
190
  </div>
168
191
  </Header.Search>
169
192
  <Header.Spacer />
@@ -106,9 +106,122 @@
106
106
  }
107
107
 
108
108
  .navItemActive {
109
+ // Match sidebar active state: secondary surface + primary text
109
110
  color: var(--fui-text-primary, $fui-text-primary);
110
111
  font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
111
- background-color: var(--fui-bg-hover, $fui-bg-hover);
112
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary);
113
+
114
+ &:hover {
115
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary) !important;
116
+ color: var(--fui-text-primary, $fui-text-primary) !important;
117
+ }
118
+
119
+ &:active {
120
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary) !important;
121
+ color: var(--fui-text-primary, $fui-text-primary) !important;
122
+ }
123
+ }
124
+
125
+ // ============================================
126
+ // NavMenu (dropdown trigger)
127
+ // ============================================
128
+
129
+ .navMenuTrigger {
130
+ gap: var(--fui-space-1, $fui-space-1);
131
+ cursor: pointer;
132
+ }
133
+
134
+ .navMenuChevron {
135
+ transition: transform var(--fui-transition-fast, $fui-transition-fast);
136
+ flex-shrink: 0;
137
+
138
+ [data-popup-open] > & {
139
+ transform: rotate(180deg);
140
+ }
141
+ }
142
+
143
+ // ============================================
144
+ // NavMenu Popup (dropdown content)
145
+ // ============================================
146
+
147
+ .navMenuPositioner {
148
+ z-index: 52;
149
+ outline: none;
150
+ }
151
+
152
+ .navMenuPopup {
153
+ @include surface-elevated;
154
+
155
+ min-width: 10rem;
156
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
157
+ box-shadow: var(--fui-shadow-md, $fui-shadow-md);
158
+
159
+ opacity: 0;
160
+ transform: scale(0.95);
161
+ transform-origin: var(--transform-origin);
162
+ transition:
163
+ opacity var(--fui-transition-fast, $fui-transition-fast),
164
+ transform var(--fui-transition-fast, $fui-transition-fast);
165
+
166
+ &[data-open] {
167
+ opacity: 1;
168
+ transform: scale(1);
169
+ }
170
+
171
+ &[data-starting-style],
172
+ &[data-ending-style] {
173
+ opacity: 0;
174
+ transform: scale(0.95);
175
+ }
176
+ }
177
+
178
+ .navMenuItem {
179
+ @include button-reset;
180
+ @include text-base;
181
+
182
+ display: flex;
183
+ align-items: center;
184
+ width: 100%;
185
+ padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-md, $fui-padding-item-md);
186
+ border-radius: var(--fui-radius-sm, $fui-radius-sm);
187
+ color: var(--fui-text-secondary, $fui-text-secondary);
188
+ text-decoration: none;
189
+ white-space: nowrap;
190
+ cursor: pointer;
191
+ outline: none;
192
+
193
+ &[data-highlighted] {
194
+ background-color: var(--fui-bg-hover, $fui-bg-hover);
195
+ color: var(--fui-text-primary, $fui-text-primary);
196
+ }
197
+ }
198
+
199
+ .navMenuItemActive {
200
+ // Match active nav item treatment in both light and dark themes
201
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary);
202
+ color: var(--fui-text-primary, $fui-text-primary);
203
+ font-weight: var(--fui-font-weight-medium, $fui-font-weight-medium);
204
+
205
+ &[data-highlighted] {
206
+ background-color: var(--fui-bg-secondary, $fui-bg-secondary);
207
+ color: var(--fui-text-primary, $fui-text-primary);
208
+ }
209
+ }
210
+
211
+ @media (prefers-reduced-motion: reduce) {
212
+ .navMenuPopup {
213
+ transition: none;
214
+ transform: none;
215
+
216
+ &[data-starting-style],
217
+ &[data-ending-style] {
218
+ transform: none;
219
+ }
220
+ }
221
+
222
+ .navMenuChevron {
223
+ transition: none;
224
+ }
112
225
  }
113
226
 
114
227
  // ============================================
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { render, screen, expectNoA11yViolations } from '../../test/utils';
2
+ import { render, screen, userEvent, waitFor, expectNoA11yViolations } from '../../test/utils';
3
3
  import { Header } from './index';
4
4
 
5
5
  describe('Header', () => {
@@ -81,3 +81,108 @@ describe('Header', () => {
81
81
  await expectNoA11yViolations(container);
82
82
  });
83
83
  });
84
+
85
+ describe('Header.NavMenu', () => {
86
+ it('renders a trigger button with the label', () => {
87
+ render(
88
+ <Header>
89
+ <Header.Nav>
90
+ <Header.NavMenu label="Docs">
91
+ <Header.NavMenuItem href="/cli">CLI</Header.NavMenuItem>
92
+ </Header.NavMenu>
93
+ </Header.Nav>
94
+ </Header>
95
+ );
96
+ expect(screen.getByRole('button', { name: /Docs/ })).toBeInTheDocument();
97
+ });
98
+
99
+ it('opens dropdown on click and shows menu items', async () => {
100
+ const user = userEvent.setup();
101
+ render(
102
+ <Header>
103
+ <Header.Nav>
104
+ <Header.NavMenu label="Docs">
105
+ <Header.NavMenuItem href="/cli">CLI Reference</Header.NavMenuItem>
106
+ <Header.NavMenuItem href="/mcp">MCP Tools</Header.NavMenuItem>
107
+ </Header.NavMenu>
108
+ </Header.Nav>
109
+ </Header>
110
+ );
111
+
112
+ await user.click(screen.getByRole('button', { name: /Docs/ }));
113
+ await waitFor(() => {
114
+ expect(screen.getByText('CLI Reference')).toBeInTheDocument();
115
+ expect(screen.getByText('MCP Tools')).toBeInTheDocument();
116
+ });
117
+ });
118
+
119
+ it('applies active class to trigger when active prop is true', () => {
120
+ render(
121
+ <Header>
122
+ <Header.Nav>
123
+ <Header.NavMenu label="Docs" active>
124
+ <Header.NavMenuItem href="/cli">CLI</Header.NavMenuItem>
125
+ </Header.NavMenu>
126
+ </Header.Nav>
127
+ </Header>
128
+ );
129
+ const trigger = screen.getByRole('button', { name: /Docs/ });
130
+ expect(trigger.className).toMatch(/navItemActive/);
131
+ });
132
+
133
+ it('renders NavMenuItem with href as a link', async () => {
134
+ const user = userEvent.setup();
135
+ render(
136
+ <Header>
137
+ <Header.Nav>
138
+ <Header.NavMenu label="Docs">
139
+ <Header.NavMenuItem href="/getting-started">Getting Started</Header.NavMenuItem>
140
+ </Header.NavMenu>
141
+ </Header.Nav>
142
+ </Header>
143
+ );
144
+
145
+ await user.click(screen.getByRole('button', { name: /Docs/ }));
146
+ await waitFor(() => {
147
+ const item = screen.getByText('Getting Started');
148
+ const link = item.closest('a') || item;
149
+ expect(link).toHaveAttribute('href', '/getting-started');
150
+ });
151
+ });
152
+
153
+ it('applies active class to NavMenuItem when active', async () => {
154
+ const user = userEvent.setup();
155
+ render(
156
+ <Header>
157
+ <Header.Nav>
158
+ <Header.NavMenu label="Docs">
159
+ <Header.NavMenuItem href="/cli" active>CLI</Header.NavMenuItem>
160
+ </Header.NavMenu>
161
+ </Header.Nav>
162
+ </Header>
163
+ );
164
+
165
+ await user.click(screen.getByRole('button', { name: /Docs/ }));
166
+ await waitFor(() => {
167
+ const item = screen.getByText('CLI');
168
+ expect(item.className).toMatch(/navMenuItemActive/);
169
+ });
170
+ });
171
+
172
+ it('has no accessibility violations', async () => {
173
+ const { container } = render(
174
+ <Header>
175
+ <Header.Nav aria-label="Main">
176
+ <Header.NavItem href="/home">Home</Header.NavItem>
177
+ <Header.NavMenu label="Docs">
178
+ <Header.NavMenuItem href="/cli">CLI</Header.NavMenuItem>
179
+ <Header.NavMenuItem href="/mcp">MCP</Header.NavMenuItem>
180
+ </Header.NavMenu>
181
+ </Header.Nav>
182
+ </Header>
183
+ );
184
+ await expectNoA11yViolations(container, {
185
+ disabledRules: ['aria-command-name'],
186
+ });
187
+ });
188
+ });
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
+ import { Menu as BaseMenu } from '@base-ui/react/menu';
5
+ import { CaretDown, List, X } from '@phosphor-icons/react';
4
6
  import styles from './Header.module.scss';
5
7
  import { useSidebar } from '../Sidebar';
6
8
  // Import globals to ensure CSS variables are defined
@@ -71,38 +73,26 @@ export interface HeaderTriggerProps {
71
73
  className?: string;
72
74
  }
73
75
 
74
- // ============================================
75
- // Icons
76
- // ============================================
77
-
78
- function MenuIcon() {
79
- return (
80
- <svg
81
- xmlns="http://www.w3.org/2000/svg"
82
- width="24"
83
- height="24"
84
- viewBox="0 0 256 256"
85
- fill="currentColor"
86
- aria-hidden="true"
87
- >
88
- <path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z" />
89
- </svg>
90
- );
76
+ export interface HeaderNavMenuProps {
77
+ /** Trigger label text */
78
+ label: string;
79
+ /** Whether any child in the group is active */
80
+ active?: boolean;
81
+ /** Additional class name */
82
+ className?: string;
83
+ children: React.ReactNode;
91
84
  }
92
85
 
93
- function CloseIcon() {
94
- return (
95
- <svg
96
- xmlns="http://www.w3.org/2000/svg"
97
- width="24"
98
- height="24"
99
- viewBox="0 0 256 256"
100
- fill="currentColor"
101
- aria-hidden="true"
102
- >
103
- <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" />
104
- </svg>
105
- );
86
+ export interface HeaderNavMenuItemProps {
87
+ children: React.ReactNode;
88
+ /** Link destination */
89
+ href?: string;
90
+ /** Whether this item is active/current */
91
+ active?: boolean;
92
+ /** Render as child element (polymorphic) */
93
+ asChild?: boolean;
94
+ /** Additional class name */
95
+ className?: string;
106
96
  }
107
97
 
108
98
  // ============================================
@@ -302,7 +292,7 @@ function HeaderTrigger({
302
292
  aria-label={ariaLabel}
303
293
  aria-expanded={open}
304
294
  >
305
- {children || (open ? <CloseIcon /> : <MenuIcon />)}
295
+ {children || (open ? <X size={24} aria-hidden /> : <List size={24} aria-hidden />)}
306
296
  </button>
307
297
  );
308
298
  }
@@ -315,6 +305,81 @@ function HeaderSpacer({ className }: { className?: string }) {
315
305
  return <div className={classes} />;
316
306
  }
317
307
 
308
+ /**
309
+ * Header.NavMenu - Dropdown navigation group
310
+ */
311
+ function HeaderNavMenu({
312
+ label,
313
+ active = false,
314
+ className,
315
+ children,
316
+ }: HeaderNavMenuProps) {
317
+ const triggerClasses = [
318
+ styles.navItem,
319
+ styles.navMenuTrigger,
320
+ active && styles.navItemActive,
321
+ className,
322
+ ].filter(Boolean).join(' ');
323
+
324
+ return (
325
+ <li>
326
+ <BaseMenu.Root modal={false}>
327
+ <BaseMenu.Trigger className={triggerClasses}>
328
+ {label}
329
+ <CaretDown size={12} className={styles.navMenuChevron} aria-hidden />
330
+ </BaseMenu.Trigger>
331
+ <BaseMenu.Portal>
332
+ <BaseMenu.Positioner side="bottom" align="start" sideOffset={4} className={styles.navMenuPositioner}>
333
+ <BaseMenu.Popup className={styles.navMenuPopup}>
334
+ {children}
335
+ </BaseMenu.Popup>
336
+ </BaseMenu.Positioner>
337
+ </BaseMenu.Portal>
338
+ </BaseMenu.Root>
339
+ </li>
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Header.NavMenuItem - Item inside a NavMenu dropdown
345
+ */
346
+ function HeaderNavMenuItem({
347
+ children,
348
+ href,
349
+ active = false,
350
+ asChild = false,
351
+ className,
352
+ }: HeaderNavMenuItemProps) {
353
+ const classes = [
354
+ styles.navMenuItem,
355
+ active && styles.navMenuItemActive,
356
+ className,
357
+ ].filter(Boolean).join(' ');
358
+
359
+ if (asChild && React.isValidElement(children)) {
360
+ return (
361
+ <BaseMenu.Item
362
+ className={classes}
363
+ render={children as React.ReactElement}
364
+ />
365
+ );
366
+ }
367
+
368
+ if (href) {
369
+ return (
370
+ <BaseMenu.Item className={classes} render={<a href={href} />}>
371
+ {children}
372
+ </BaseMenu.Item>
373
+ );
374
+ }
375
+
376
+ return (
377
+ <BaseMenu.Item className={classes}>
378
+ {children}
379
+ </BaseMenu.Item>
380
+ );
381
+ }
382
+
318
383
  /**
319
384
  * Header.SkipLink - Skip to main content link (accessibility)
320
385
  */
@@ -343,6 +408,8 @@ export const Header = Object.assign(HeaderRoot, {
343
408
  Brand: HeaderBrand,
344
409
  Nav: HeaderNav,
345
410
  NavItem: HeaderNavItem,
411
+ NavMenu: HeaderNavMenu,
412
+ NavMenuItem: HeaderNavMenuItem,
346
413
  Search: HeaderSearch,
347
414
  Actions: HeaderActions,
348
415
  Trigger: HeaderTrigger,
@@ -355,6 +422,8 @@ export {
355
422
  HeaderBrand,
356
423
  HeaderNav,
357
424
  HeaderNavItem,
425
+ HeaderNavMenu,
426
+ HeaderNavMenuItem,
358
427
  HeaderSearch,
359
428
  HeaderActions,
360
429
  HeaderTrigger,
@@ -44,7 +44,7 @@ export default defineSegment({
44
44
 
45
45
  props: {
46
46
  icon: {
47
- type: 'component',
47
+ type: 'custom',
48
48
  description: 'Phosphor icon component to render',
49
49
  required: true,
50
50
  },
@@ -66,6 +66,11 @@ export default defineSegment({
66
66
  values: ['default', 'primary', 'secondary', 'tertiary', 'accent', 'success', 'warning', 'error'],
67
67
  default: 'default',
68
68
  },
69
+ color: {
70
+ type: 'enum',
71
+ description: 'Deprecated alias for variant',
72
+ values: ['primary', 'secondary', 'tertiary', 'accent', 'success', 'warning', 'error'],
73
+ },
69
74
  },
70
75
 
71
76
  relations: [
@@ -24,7 +24,7 @@ const sizeMap: Record<NonNullable<IconProps['size']>, number> = {
24
24
  xl: 32,
25
25
  };
26
26
 
27
- export const Icon = React.forwardRef<HTMLSpanElement, IconProps>(
27
+ const IconRoot = React.forwardRef<HTMLSpanElement, IconProps>(
28
28
  function Icon(
29
29
  {
30
30
  icon: IconComponent,
@@ -56,3 +56,7 @@ export const Icon = React.forwardRef<HTMLSpanElement, IconProps>(
56
56
  );
57
57
  }
58
58
  );
59
+
60
+ export const Icon = Object.assign(IconRoot, {
61
+ Root: IconRoot,
62
+ });
@@ -65,11 +65,11 @@ export default defineSegment({
65
65
  default: 'cover',
66
66
  },
67
67
  width: {
68
- type: 'string | number',
68
+ type: 'union',
69
69
  description: 'Width of the image container',
70
70
  },
71
71
  height: {
72
- type: 'string | number',
72
+ type: 'union',
73
73
  description: 'Height of the image container',
74
74
  },
75
75
  rounded: {
@@ -27,7 +27,7 @@ export interface ImageProps {
27
27
  style?: React.CSSProperties;
28
28
  }
29
29
 
30
- export const Image = React.forwardRef<HTMLDivElement, ImageProps>(
30
+ const ImageRoot = React.forwardRef<HTMLDivElement, ImageProps>(
31
31
  function Image(
32
32
  {
33
33
  src,
@@ -93,3 +93,7 @@ export const Image = React.forwardRef<HTMLDivElement, ImageProps>(
93
93
  );
94
94
  }
95
95
  );
96
+
97
+ export const Image = Object.assign(ImageRoot, {
98
+ Root: ImageRoot,
99
+ });
@@ -23,7 +23,7 @@ export default defineSegment({
23
23
  whenNot: [
24
24
  'Multi-line text (use Textarea)',
25
25
  'Selecting from predefined options (use Select)',
26
- 'Boolean input (use Checkbox or Switch)',
26
+ 'Boolean input (use Checkbox or Toggle)',
27
27
  'Date/time input (use DatePicker)',
28
28
  ],
29
29
  guidelines: [
@@ -55,6 +55,12 @@ export default defineSegment({
55
55
  default: 'text',
56
56
  description: 'HTML input type for validation and keyboard',
57
57
  },
58
+ size: {
59
+ type: 'enum',
60
+ values: ['sm', 'md', 'lg'],
61
+ default: 'md',
62
+ description: 'Size variant',
63
+ },
58
64
  disabled: {
59
65
  type: 'boolean',
60
66
  default: false,
@@ -73,10 +79,22 @@ export default defineSegment({
73
79
  type: 'string',
74
80
  description: 'Helper or error message below input',
75
81
  },
82
+ shortcut: {
83
+ type: 'string',
84
+ description: 'Keyboard shortcut hint displayed inside the input',
85
+ },
76
86
  onChange: {
77
87
  type: 'function',
78
88
  description: 'Called with new value on change',
79
89
  },
90
+ inputStyle: {
91
+ type: 'object',
92
+ description: 'Inline styles applied directly to the input element',
93
+ },
94
+ inputClassName: {
95
+ type: 'string',
96
+ description: 'Class name applied directly to the input element',
97
+ },
80
98
  },
81
99
 
82
100
  relations: [
@@ -91,9 +109,9 @@ export default defineSegment({
91
109
  note: 'Use Select when choosing from predefined options',
92
110
  },
93
111
  {
94
- component: 'FormField',
112
+ component: 'Field',
95
113
  relationship: 'parent',
96
- note: 'Wrap in FormField for consistent form layout',
114
+ note: 'Use Field for advanced form composition and custom controls',
97
115
  },
98
116
  ],
99
117
 
@@ -109,7 +109,7 @@
109
109
  padding: 2px 6px;
110
110
  font-size: var(--fui-font-size-xs, 0.75rem);
111
111
  font-family: var(--fui-font-mono, ui-monospace, monospace);
112
- color: var(--fui-text-tertiary);
112
+ color: var(--fui-text-secondary);
113
113
  background: var(--fui-bg-subtle);
114
114
  border: 1px solid var(--fui-border-default);
115
115
  border-radius: var(--fui-radius-sm, 0.25rem);
@@ -33,7 +33,7 @@ function mergeAriaIds(...ids: Array<string | undefined>): string | undefined {
33
33
  return merged.length > 0 ? merged : undefined;
34
34
  }
35
35
 
36
- export const Input = React.forwardRef<HTMLInputElement, InputProps>(
36
+ const InputRoot = React.forwardRef<HTMLInputElement, InputProps>(
37
37
  function Input(
38
38
  {
39
39
  value,
@@ -127,3 +127,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
127
127
  );
128
128
  }
129
129
  );
130
+
131
+ export const Input = Object.assign(InputRoot, {
132
+ Root: InputRoot,
133
+ });
@@ -47,10 +47,6 @@ export default defineSegment({
47
47
  description: 'Link text content',
48
48
  required: true,
49
49
  },
50
- href: {
51
- type: 'string',
52
- description: 'URL destination',
53
- },
54
50
  variant: {
55
51
  type: 'enum',
56
52
  description: 'Visual style variant',