@fragments-sdk/ui 0.7.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -25
- package/fragments.json +1 -1
- package/package.json +15 -5
- package/src/blocks/AppShell.block.ts +2 -2
- package/src/blocks/InsetDashboardLayout.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +14 -7
- package/src/components/Accordion/Accordion.fragment.tsx +10 -4
- package/src/components/Alert/Alert.fragment.tsx +2 -2
- package/src/components/Alert/Alert.module.scss +4 -4
- package/src/components/AppShell/AppShell.fragment.tsx +3 -3
- package/src/components/AppShell/index.tsx +2 -0
- package/src/components/Avatar/Avatar.fragment.tsx +7 -3
- package/src/components/Avatar/Avatar.module.scss +1 -1
- package/src/components/Avatar/index.tsx +37 -1
- package/src/components/Badge/Badge.fragment.tsx +5 -5
- package/src/components/Badge/Badge.module.scss +4 -4
- package/src/components/Badge/index.tsx +5 -1
- package/src/components/Box/Box.fragment.tsx +2 -2
- package/src/components/Box/index.tsx +5 -1
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +2 -2
- package/src/components/Button/Button.fragment.tsx +19 -18
- package/src/components/Button/index.tsx +5 -1
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +2 -2
- package/src/components/ButtonGroup/index.tsx +5 -1
- package/src/components/Card/Card.fragment.tsx +7 -7
- package/src/components/Chart/Chart.fragment.tsx +11 -3
- package/src/components/Chart/index.tsx +22 -4
- package/src/components/Checkbox/Checkbox.fragment.tsx +2 -2
- package/src/components/Checkbox/index.tsx +5 -1
- package/src/components/Chip/Chip.fragment.tsx +2 -7
- package/src/components/Chip/Chip.module.scss +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +11 -5
- package/src/components/CodeBlock/CodeBlock.module.scss +11 -53
- package/src/components/CodeBlock/index.tsx +13 -24
- package/src/components/Collapsible/Collapsible.fragment.tsx +2 -2
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +2 -2
- package/src/components/ColorPicker/index.tsx +5 -1
- package/src/components/Combobox/Combobox.fragment.tsx +17 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +5 -5
- package/src/components/ConversationList/ConversationList.module.scss +1 -1
- package/src/components/DatePicker/DatePicker.fragment.tsx +245 -0
- package/src/components/DatePicker/DatePicker.module.scss +394 -0
- package/src/components/DatePicker/DatePicker.test.tsx +264 -0
- package/src/components/DatePicker/index.tsx +535 -0
- package/src/components/Dialog/Dialog.fragment.tsx +2 -2
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +7 -6
- package/src/components/Fieldset/Fieldset.fragment.tsx +7 -6
- package/src/components/Form/Form.fragment.tsx +11 -5
- package/src/components/Form/index.tsx +5 -1
- package/src/components/Grid/Grid.fragment.tsx +6 -2
- package/src/components/Header/Header.fragment.tsx +38 -15
- package/src/components/Header/Header.module.scss +114 -1
- package/src/components/Header/Header.test.tsx +106 -1
- package/src/components/Header/index.tsx +100 -31
- package/src/components/Icon/Icon.fragment.tsx +8 -3
- package/src/components/Icon/index.tsx +5 -1
- package/src/components/Image/Image.fragment.tsx +4 -4
- package/src/components/Image/index.tsx +5 -1
- package/src/components/Input/Input.fragment.tsx +23 -5
- package/src/components/Input/Input.module.scss +1 -1
- package/src/components/Input/index.tsx +5 -1
- package/src/components/Link/Link.fragment.tsx +2 -6
- package/src/components/Link/index.tsx +5 -1
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +2 -14
- package/src/components/Loading/Loading.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.module.scss +11 -3
- package/src/components/Markdown/index.tsx +5 -1
- package/src/components/Menu/Menu.fragment.tsx +2 -2
- package/src/components/Message/Message.fragment.tsx +10 -8
- package/src/components/Message/Message.module.scss +1 -1
- package/src/components/Popover/Popover.fragment.tsx +2 -2
- package/src/components/Progress/Progress.fragment.tsx +16 -2
- package/src/components/Progress/index.tsx +9 -2
- package/src/components/Prompt/Prompt.fragment.tsx +13 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +7 -2
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +185 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +136 -0
- package/src/components/ScrollArea/ScrollArea.test.tsx +38 -0
- package/src/components/ScrollArea/index.tsx +121 -0
- package/src/components/Select/Select.fragment.tsx +15 -7
- package/src/components/Separator/Separator.fragment.tsx +2 -2
- package/src/components/Separator/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +66 -13
- package/src/components/Sidebar/Sidebar.module.scss +69 -21
- package/src/components/Sidebar/Sidebar.test.tsx +31 -2
- package/src/components/Sidebar/index.tsx +69 -45
- package/src/components/Skeleton/Skeleton.fragment.tsx +7 -2
- package/src/components/Slider/Slider.fragment.tsx +2 -2
- package/src/components/Slider/index.tsx +5 -1
- package/src/components/Stack/Stack.fragment.tsx +4 -4
- package/src/components/Stack/index.tsx +5 -1
- package/src/components/Table/Table.fragment.tsx +31 -2
- package/src/components/Table/index.tsx +49 -6
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +66 -0
- package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
- package/src/components/TableOfContents/index.tsx +110 -0
- package/src/components/Tabs/Tabs.fragment.tsx +2 -2
- package/src/components/Text/Text.fragment.tsx +2 -2
- package/src/components/Text/Text.module.scss +6 -0
- package/src/components/Text/Text.test.tsx +5 -0
- package/src/components/Text/index.tsx +8 -1
- package/src/components/Textarea/Textarea.fragment.tsx +10 -2
- package/src/components/Textarea/index.tsx +5 -1
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +1 -1
- package/src/components/Theme/index.tsx +8 -1
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +5 -4
- package/src/components/Toast/Toast.fragment.tsx +14 -2
- package/src/components/Toggle/Toggle.fragment.tsx +2 -2
- package/src/components/Toggle/index.tsx +5 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +5 -5
- package/src/components/Tooltip/Tooltip.fragment.tsx +20 -2
- package/src/components/Tooltip/index.tsx +6 -1
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/components/VisuallyHidden/index.tsx +5 -1
- package/src/components/compound-pattern.test.ts +40 -0
- package/src/index.ts +29 -0
- package/src/recipes/AppShell.recipe.ts +2 -2
- package/src/recipes/LoginForm.recipe.ts +14 -7
- package/src/tokens/_computed.scss +12 -0
- package/src/tokens/_derive.scss +71 -0
- package/src/tokens/_mixins.scss +9 -0
- package/src/tokens/_variables.scss +26 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Grid } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default defineFragment({
|
|
6
6
|
component: Grid,
|
|
7
7
|
|
|
8
8
|
meta: {
|
|
@@ -39,6 +39,10 @@ export default defineSegment({
|
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
props: {
|
|
42
|
+
children: {
|
|
43
|
+
type: 'node',
|
|
44
|
+
description: 'Grid items and content',
|
|
45
|
+
},
|
|
42
46
|
columns: {
|
|
43
47
|
type: 'union',
|
|
44
48
|
description: 'Number of columns: a number (1-12), a responsive object { base, sm, md, lg, xl }, or "auto" for auto-fill',
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Header } from '.';
|
|
4
4
|
import { ThemeToggle, ThemeProvider } from '../Theme';
|
|
5
5
|
import { Button } from '../Button';
|
|
6
6
|
import { Input } from '../Input';
|
|
7
|
+
import { MagnifyingGlass } from '@phosphor-icons/react';
|
|
7
8
|
|
|
8
|
-
|
|
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
|
-
}
|
|
15
|
-
|
|
16
|
-
export default defineSegment({
|
|
9
|
+
export default defineFragment({
|
|
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: '
|
|
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
|
-
<
|
|
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-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 ? <
|
|
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,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Icon } from '.';
|
|
4
4
|
import { Heart, Star, Check, Warning, Info } from '@phosphor-icons/react';
|
|
5
5
|
|
|
6
|
-
export default
|
|
6
|
+
export default defineFragment({
|
|
7
7
|
component: Icon,
|
|
8
8
|
|
|
9
9
|
meta: {
|
|
@@ -44,7 +44,7 @@ export default defineSegment({
|
|
|
44
44
|
|
|
45
45
|
props: {
|
|
46
46
|
icon: {
|
|
47
|
-
type: '
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defineFragment } from '@fragments/core';
|
|
3
3
|
import { Image } from '.';
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default defineFragment({
|
|
6
6
|
component: Image,
|
|
7
7
|
|
|
8
8
|
meta: {
|
|
@@ -65,11 +65,11 @@ export default defineSegment({
|
|
|
65
65
|
default: 'cover',
|
|
66
66
|
},
|
|
67
67
|
width: {
|
|
68
|
-
type: '
|
|
68
|
+
type: 'union',
|
|
69
69
|
description: 'Width of the image container',
|
|
70
70
|
},
|
|
71
71
|
height: {
|
|
72
|
-
type: '
|
|
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
|
-
|
|
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
|
+
});
|