@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.
- 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 +8 -2
- package/src/components/Alert/Alert.module.scss +4 -4
- package/src/components/AppShell/AppShell.fragment.tsx +1 -1
- package/src/components/AppShell/index.tsx +2 -0
- package/src/components/Avatar/Avatar.fragment.tsx +5 -1
- package/src/components/Avatar/Avatar.module.scss +1 -1
- package/src/components/Avatar/index.tsx +37 -1
- package/src/components/Badge/Badge.fragment.tsx +3 -3
- package/src/components/Badge/Badge.module.scss +4 -4
- package/src/components/Badge/index.tsx +5 -1
- package/src/components/Box/index.tsx +5 -1
- package/src/components/Button/Button.fragment.tsx +17 -16
- package/src/components/Button/index.tsx +5 -1
- package/src/components/ButtonGroup/index.tsx +5 -1
- package/src/components/Card/Card.fragment.tsx +5 -5
- package/src/components/Chart/Chart.fragment.tsx +9 -1
- package/src/components/Chart/index.tsx +22 -4
- package/src/components/Checkbox/index.tsx +5 -1
- package/src/components/Chip/Chip.fragment.tsx +0 -5
- package/src/components/Chip/Chip.module.scss +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +9 -3
- package/src/components/CodeBlock/CodeBlock.module.scss +1 -1
- package/src/components/ColorPicker/index.tsx +5 -1
- package/src/components/Combobox/Combobox.fragment.tsx +15 -7
- package/src/components/ConversationList/ConversationList.fragment.tsx +3 -3
- 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/Field/Field.fragment.tsx +5 -4
- package/src/components/Fieldset/Fieldset.fragment.tsx +5 -4
- package/src/components/Form/Form.fragment.tsx +9 -3
- package/src/components/Form/index.tsx +5 -1
- package/src/components/Grid/Grid.fragment.tsx +4 -0
- package/src/components/Header/Header.fragment.tsx +36 -13
- 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 +6 -1
- package/src/components/Icon/index.tsx +5 -1
- package/src/components/Image/Image.fragment.tsx +2 -2
- package/src/components/Image/index.tsx +5 -1
- package/src/components/Input/Input.fragment.tsx +21 -3
- package/src/components/Input/Input.module.scss +1 -1
- package/src/components/Input/index.tsx +5 -1
- package/src/components/Link/Link.fragment.tsx +0 -4
- package/src/components/Link/index.tsx +5 -1
- package/src/components/Listbox/Listbox.fragment.tsx +0 -12
- package/src/components/Markdown/Markdown.module.scss +6 -3
- package/src/components/Markdown/index.tsx +5 -1
- package/src/components/Message/Message.fragment.tsx +8 -6
- package/src/components/Message/Message.module.scss +1 -1
- package/src/components/Progress/Progress.fragment.tsx +14 -0
- package/src/components/Progress/index.tsx +9 -2
- package/src/components/Prompt/Prompt.fragment.tsx +11 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +5 -0
- 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 +13 -5
- package/src/components/Separator/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +64 -11
- package/src/components/Sidebar/Sidebar.module.scss +68 -16
- package/src/components/Sidebar/Sidebar.test.tsx +31 -2
- package/src/components/Sidebar/index.tsx +69 -45
- package/src/components/Skeleton/Skeleton.fragment.tsx +5 -0
- package/src/components/Slider/index.tsx +5 -1
- package/src/components/Stack/Stack.fragment.tsx +2 -2
- package/src/components/Stack/index.tsx +5 -1
- package/src/components/Table/Table.fragment.tsx +29 -0
- package/src/components/Table/index.tsx +6 -1
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +71 -0
- package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
- package/src/components/TableOfContents/index.tsx +105 -0
- package/src/components/Text/index.tsx +5 -1
- package/src/components/Textarea/Textarea.fragment.tsx +8 -0
- package/src/components/Textarea/index.tsx +5 -1
- package/src/components/Theme/index.tsx +7 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +3 -2
- package/src/components/Toast/Toast.fragment.tsx +12 -0
- package/src/components/Toggle/index.tsx +5 -1
- package/src/components/Tooltip/Tooltip.fragment.tsx +18 -0
- package/src/components/Tooltip/index.tsx +6 -1
- 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/_variables.scss +22 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './ScrollArea.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/** Scroll direction */
|
|
12
|
+
orientation?: 'horizontal' | 'vertical' | 'both';
|
|
13
|
+
/** Scrollbar visibility behavior */
|
|
14
|
+
scrollbarVisibility?: 'auto' | 'always' | 'hover';
|
|
15
|
+
/** Whether to show fade indicators at scroll edges */
|
|
16
|
+
showFades?: boolean;
|
|
17
|
+
/** Additional class name */
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// Component
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ScrollArea - A styled scrollable container with customizable scrollbars.
|
|
27
|
+
*
|
|
28
|
+
* Provides thin, unobtrusive scrollbars that appear on hover or scroll,
|
|
29
|
+
* with optional fade indicators to hint at overflowing content.
|
|
30
|
+
*/
|
|
31
|
+
function ScrollAreaRoot({
|
|
32
|
+
children,
|
|
33
|
+
orientation = 'vertical',
|
|
34
|
+
scrollbarVisibility = 'auto',
|
|
35
|
+
showFades = false,
|
|
36
|
+
className,
|
|
37
|
+
...htmlProps
|
|
38
|
+
}: ScrollAreaProps) {
|
|
39
|
+
const viewportRef = React.useRef<HTMLDivElement>(null);
|
|
40
|
+
const [canScrollStart, setCanScrollStart] = React.useState(false);
|
|
41
|
+
const [canScrollEnd, setCanScrollEnd] = React.useState(false);
|
|
42
|
+
|
|
43
|
+
const updateScrollState = React.useCallback(() => {
|
|
44
|
+
const el = viewportRef.current;
|
|
45
|
+
if (!el || !showFades) return;
|
|
46
|
+
|
|
47
|
+
if (orientation === 'horizontal') {
|
|
48
|
+
setCanScrollStart(el.scrollLeft > 1);
|
|
49
|
+
setCanScrollEnd(el.scrollLeft < el.scrollWidth - el.clientWidth - 1);
|
|
50
|
+
} else {
|
|
51
|
+
setCanScrollStart(el.scrollTop > 1);
|
|
52
|
+
setCanScrollEnd(el.scrollTop < el.scrollHeight - el.clientHeight - 1);
|
|
53
|
+
}
|
|
54
|
+
}, [orientation, showFades]);
|
|
55
|
+
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const el = viewportRef.current;
|
|
58
|
+
if (!el || !showFades) return;
|
|
59
|
+
|
|
60
|
+
// Defer initial check to ensure children have laid out
|
|
61
|
+
const raf = requestAnimationFrame(updateScrollState);
|
|
62
|
+
|
|
63
|
+
el.addEventListener('scroll', updateScrollState, { passive: true });
|
|
64
|
+
|
|
65
|
+
// Observe both the viewport AND its children for size changes
|
|
66
|
+
// (viewport size stays constant when children overflow — only scrollWidth changes)
|
|
67
|
+
const observer = new ResizeObserver(updateScrollState);
|
|
68
|
+
observer.observe(el);
|
|
69
|
+
Array.from(el.children).forEach(child => observer.observe(child));
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cancelAnimationFrame(raf);
|
|
73
|
+
el.removeEventListener('scroll', updateScrollState);
|
|
74
|
+
observer.disconnect();
|
|
75
|
+
};
|
|
76
|
+
}, [showFades, updateScrollState]);
|
|
77
|
+
|
|
78
|
+
const rootClasses = [
|
|
79
|
+
styles.root,
|
|
80
|
+
className,
|
|
81
|
+
].filter(Boolean).join(' ');
|
|
82
|
+
|
|
83
|
+
// Determine which fade mask to apply to the viewport
|
|
84
|
+
const fadeMask = showFades
|
|
85
|
+
? canScrollStart && canScrollEnd
|
|
86
|
+
? styles.fadeMaskBoth
|
|
87
|
+
: canScrollStart
|
|
88
|
+
? styles.fadeMaskStart
|
|
89
|
+
: canScrollEnd
|
|
90
|
+
? styles.fadeMaskEnd
|
|
91
|
+
: undefined
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
const viewportClasses = [
|
|
95
|
+
styles.viewport,
|
|
96
|
+
styles[orientation],
|
|
97
|
+
scrollbarVisibility === 'always' && styles.scrollbarAlways,
|
|
98
|
+
scrollbarVisibility === 'hover' && styles.scrollbarHover,
|
|
99
|
+
fadeMask,
|
|
100
|
+
].filter(Boolean).join(' ');
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
{...htmlProps}
|
|
105
|
+
className={rootClasses}
|
|
106
|
+
data-orientation={orientation}
|
|
107
|
+
>
|
|
108
|
+
<div ref={viewportRef} className={viewportClasses}>
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================
|
|
116
|
+
// Export
|
|
117
|
+
// ============================================
|
|
118
|
+
|
|
119
|
+
export const ScrollArea = Object.assign(ScrollAreaRoot, {
|
|
120
|
+
Root: ScrollAreaRoot,
|
|
121
|
+
});
|
|
@@ -72,6 +72,19 @@ export default defineSegment({
|
|
|
72
72
|
type: 'function',
|
|
73
73
|
description: 'Called when selection changes',
|
|
74
74
|
},
|
|
75
|
+
open: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
description: 'Controlled open state of the dropdown',
|
|
78
|
+
},
|
|
79
|
+
defaultOpen: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: 'Initial open state for uncontrolled usage',
|
|
82
|
+
default: 'false',
|
|
83
|
+
},
|
|
84
|
+
onOpenChange: {
|
|
85
|
+
type: 'function',
|
|
86
|
+
description: 'Called when dropdown open state changes',
|
|
87
|
+
},
|
|
75
88
|
placeholder: {
|
|
76
89
|
type: 'string',
|
|
77
90
|
description: 'Placeholder text when no value selected',
|
|
@@ -81,11 +94,6 @@ export default defineSegment({
|
|
|
81
94
|
description: 'Disable the select',
|
|
82
95
|
default: 'false',
|
|
83
96
|
},
|
|
84
|
-
maxVisibleItems: {
|
|
85
|
-
type: 'number',
|
|
86
|
-
description: 'Maximum visible options before scrolling. Shows half of the next item as a scroll hint.',
|
|
87
|
-
default: '4',
|
|
88
|
-
},
|
|
89
97
|
},
|
|
90
98
|
|
|
91
99
|
relations: [
|
|
@@ -34,7 +34,7 @@ const spacingClasses = {
|
|
|
34
34
|
// Component
|
|
35
35
|
// ============================================
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const SeparatorRoot = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
38
38
|
function Separator(
|
|
39
39
|
{
|
|
40
40
|
orientation = 'horizontal',
|
|
@@ -91,3 +91,7 @@ export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
);
|
|
94
|
+
|
|
95
|
+
export const Separator = Object.assign(SeparatorRoot, {
|
|
96
|
+
Root: SeparatorRoot,
|
|
97
|
+
});
|
|
@@ -182,11 +182,12 @@ function ExternalTriggerContent() {
|
|
|
182
182
|
function ProviderDemo() {
|
|
183
183
|
return (
|
|
184
184
|
<div style={demoContainerStyle}>
|
|
185
|
-
<SidebarProvider
|
|
185
|
+
<SidebarProvider>
|
|
186
186
|
<Sidebar>
|
|
187
187
|
<Sidebar.Header collapsedContent={<LogoIcon size={32} />}>
|
|
188
188
|
<LogoIcon size={32} />
|
|
189
189
|
<span style={{ fontWeight: 600, fontSize: '16px' }}>Acme App</span>
|
|
190
|
+
<Sidebar.CollapseToggle />
|
|
190
191
|
</Sidebar.Header>
|
|
191
192
|
<Sidebar.Nav>
|
|
192
193
|
<Sidebar.Section>
|
|
@@ -195,9 +196,6 @@ function ProviderDemo() {
|
|
|
195
196
|
<Sidebar.Item icon={<UsersIcon />}>Team</Sidebar.Item>
|
|
196
197
|
</Sidebar.Section>
|
|
197
198
|
</Sidebar.Nav>
|
|
198
|
-
<Sidebar.Footer>
|
|
199
|
-
<Sidebar.CollapseToggle />
|
|
200
|
-
</Sidebar.Footer>
|
|
201
199
|
</Sidebar>
|
|
202
200
|
<ExternalTriggerContent />
|
|
203
201
|
</SidebarProvider>
|
|
@@ -322,6 +320,35 @@ function SkeletonDemo() {
|
|
|
322
320
|
);
|
|
323
321
|
}
|
|
324
322
|
|
|
323
|
+
// Demo for offcanvas collapsed state
|
|
324
|
+
function OffcanvasDemo() {
|
|
325
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
326
|
+
return (
|
|
327
|
+
<div style={demoContainerStyle}>
|
|
328
|
+
<Sidebar collapsed={collapsed} onCollapsedChange={setCollapsed} collapsible="offcanvas">
|
|
329
|
+
<Sidebar.Header collapsedContent={<LogoIcon size={32} />}>
|
|
330
|
+
<LogoIcon size={32} />
|
|
331
|
+
<span style={{ fontWeight: 600, fontSize: '16px' }}>Acme App</span>
|
|
332
|
+
</Sidebar.Header>
|
|
333
|
+
<Sidebar.Nav>
|
|
334
|
+
<Sidebar.Section>
|
|
335
|
+
<Sidebar.Item icon={<HomeIcon />} active>Dashboard</Sidebar.Item>
|
|
336
|
+
<Sidebar.Item icon={<ChartIcon />}>Analytics</Sidebar.Item>
|
|
337
|
+
<Sidebar.Item icon={<UsersIcon />}>Team</Sidebar.Item>
|
|
338
|
+
<Sidebar.Item icon={<FolderIcon />}>Projects</Sidebar.Item>
|
|
339
|
+
</Sidebar.Section>
|
|
340
|
+
</Sidebar.Nav>
|
|
341
|
+
<Sidebar.Footer>
|
|
342
|
+
<Sidebar.CollapseToggle />
|
|
343
|
+
</Sidebar.Footer>
|
|
344
|
+
</Sidebar>
|
|
345
|
+
<main style={mainContentStyle}>
|
|
346
|
+
Offcanvas mode hides sidebar completely. Toggle stays visible to re-open.
|
|
347
|
+
</main>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
325
352
|
// Demo for rail toggle
|
|
326
353
|
function RailDemo() {
|
|
327
354
|
const [collapsed, setCollapsed] = useState(false);
|
|
@@ -459,11 +486,6 @@ export default defineSegment({
|
|
|
459
486
|
values: ['icon', 'offcanvas', 'none'],
|
|
460
487
|
default: 'icon',
|
|
461
488
|
},
|
|
462
|
-
asChild: {
|
|
463
|
-
type: 'boolean',
|
|
464
|
-
description: '(Sidebar.Item) Render as child element for polymorphic composition',
|
|
465
|
-
default: false,
|
|
466
|
-
},
|
|
467
489
|
},
|
|
468
490
|
|
|
469
491
|
relations: [
|
|
@@ -690,9 +712,14 @@ export default defineSegment({
|
|
|
690
712
|
description: 'SidebarProvider enables external triggers and keyboard shortcuts (Cmd/Ctrl+B).',
|
|
691
713
|
code: `function App() {
|
|
692
714
|
return (
|
|
693
|
-
<SidebarProvider
|
|
715
|
+
<SidebarProvider>
|
|
694
716
|
<Sidebar>
|
|
695
|
-
|
|
717
|
+
<Sidebar.Header>
|
|
718
|
+
<Logo />
|
|
719
|
+
<span>Acme App</span>
|
|
720
|
+
<Sidebar.CollapseToggle />
|
|
721
|
+
</Sidebar.Header>
|
|
722
|
+
{/* sidebar nav */}
|
|
696
723
|
</Sidebar>
|
|
697
724
|
<MainContent />
|
|
698
725
|
</SidebarProvider>
|
|
@@ -779,5 +806,31 @@ function MainContent() {
|
|
|
779
806
|
</Sidebar>`,
|
|
780
807
|
render: () => <RailDemo />,
|
|
781
808
|
},
|
|
809
|
+
{
|
|
810
|
+
name: 'Offcanvas Collapsed',
|
|
811
|
+
description: 'Offcanvas mode hides the sidebar completely when collapsed, but the toggle button remains visible as a floating button so the user can always re-expand.',
|
|
812
|
+
code: `function App() {
|
|
813
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
814
|
+
|
|
815
|
+
return (
|
|
816
|
+
<Sidebar collapsed={collapsed} onCollapsedChange={setCollapsed} collapsible="offcanvas">
|
|
817
|
+
<Sidebar.Header collapsedContent={<Logo />}>
|
|
818
|
+
<Logo />
|
|
819
|
+
<span>Acme App</span>
|
|
820
|
+
</Sidebar.Header>
|
|
821
|
+
<Sidebar.Nav>
|
|
822
|
+
<Sidebar.Section>
|
|
823
|
+
<Sidebar.Item icon={<HomeIcon />} active>Dashboard</Sidebar.Item>
|
|
824
|
+
<Sidebar.Item icon={<ChartIcon />}>Analytics</Sidebar.Item>
|
|
825
|
+
</Sidebar.Section>
|
|
826
|
+
</Sidebar.Nav>
|
|
827
|
+
<Sidebar.Footer>
|
|
828
|
+
<Sidebar.CollapseToggle />
|
|
829
|
+
</Sidebar.Footer>
|
|
830
|
+
</Sidebar>
|
|
831
|
+
);
|
|
832
|
+
}`,
|
|
833
|
+
render: () => <OffcanvasDemo />,
|
|
834
|
+
},
|
|
782
835
|
],
|
|
783
836
|
});
|
|
@@ -27,11 +27,15 @@
|
|
|
27
27
|
|
|
28
28
|
// Desktop collapsed state
|
|
29
29
|
.collapsed {
|
|
30
|
-
width: var(--sidebar-collapsed-width);
|
|
30
|
+
width: var(--sidebar-effective-collapsed-width, var(--sidebar-collapsed-width));
|
|
31
31
|
|
|
32
32
|
.header {
|
|
33
33
|
justify-content: center;
|
|
34
|
-
padding: var(--fui-padding-
|
|
34
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.nav {
|
|
38
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
.sectionLabel {
|
|
@@ -46,7 +50,8 @@
|
|
|
46
50
|
|
|
47
51
|
.item {
|
|
48
52
|
justify-content: center;
|
|
49
|
-
padding: var(--fui-padding-
|
|
53
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
|
|
54
|
+
min-height: $fui-touch-md;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
.itemIcon {
|
|
@@ -55,7 +60,7 @@
|
|
|
55
60
|
|
|
56
61
|
.footer {
|
|
57
62
|
justify-content: center;
|
|
58
|
-
padding: var(--fui-padding-
|
|
63
|
+
padding: var(--fui-padding-item-xs, $fui-padding-item-xs);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
.collapseToggle {
|
|
@@ -63,6 +68,22 @@
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
.collapsedNoIcons {
|
|
72
|
+
overflow: visible;
|
|
73
|
+
|
|
74
|
+
.header,
|
|
75
|
+
.nav,
|
|
76
|
+
.footer {
|
|
77
|
+
visibility: hidden;
|
|
78
|
+
pointer-events: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.collapseToggleFloating {
|
|
82
|
+
visibility: visible;
|
|
83
|
+
pointer-events: auto;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
// Mobile state
|
|
67
88
|
.mobile {
|
|
68
89
|
position: fixed;
|
|
@@ -208,7 +229,7 @@
|
|
|
208
229
|
@include interactive-base;
|
|
209
230
|
@include text-base;
|
|
210
231
|
|
|
211
|
-
display:
|
|
232
|
+
display: flex;
|
|
212
233
|
align-items: center;
|
|
213
234
|
gap: var(--fui-space-3, $fui-space-3);
|
|
214
235
|
padding: var(--fui-padding-item-xs, $fui-padding-item-xs) var(--fui-padding-item-sm, $fui-padding-item-sm);
|
|
@@ -229,13 +250,19 @@
|
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
.itemActive {
|
|
232
|
-
// Use
|
|
233
|
-
background-color: var(--fui-
|
|
234
|
-
color: var(--fui-text-
|
|
253
|
+
// Use secondary surface + primary text so active state adapts in light and dark themes
|
|
254
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary);
|
|
255
|
+
color: var(--fui-text-primary, $fui-text-primary);
|
|
235
256
|
|
|
236
257
|
&:hover {
|
|
237
|
-
background-color: var(--fui-
|
|
238
|
-
color: var(--fui-text-
|
|
258
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary) !important;
|
|
259
|
+
color: var(--fui-text-primary, $fui-text-primary) !important;
|
|
260
|
+
text-decoration: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&:active {
|
|
264
|
+
background-color: var(--fui-bg-secondary, $fui-bg-secondary) !important;
|
|
265
|
+
color: var(--fui-text-primary, $fui-text-primary) !important;
|
|
239
266
|
text-decoration: none;
|
|
240
267
|
}
|
|
241
268
|
|
|
@@ -485,16 +512,41 @@
|
|
|
485
512
|
}
|
|
486
513
|
}
|
|
487
514
|
|
|
515
|
+
.collapseToggleFloating {
|
|
516
|
+
position: absolute;
|
|
517
|
+
top: var(--fui-space-3, $fui-space-3);
|
|
518
|
+
left: calc(100% + var(--fui-space-2, $fui-space-2));
|
|
519
|
+
z-index: 20;
|
|
520
|
+
margin: 0;
|
|
521
|
+
background-color: var(--fui-bg-primary, $fui-bg-primary);
|
|
522
|
+
border: 1px solid var(--fui-border, $fui-border);
|
|
523
|
+
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
524
|
+
|
|
525
|
+
.positionRight & {
|
|
526
|
+
left: auto;
|
|
527
|
+
right: calc(100% + var(--fui-space-2, $fui-space-2));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
488
531
|
// ============================================
|
|
489
|
-
// Offcanvas Mode (
|
|
532
|
+
// Offcanvas Mode (collapses to zero width with floating toggle)
|
|
490
533
|
// ============================================
|
|
491
534
|
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
535
|
+
.offcanvasCollapsed {
|
|
536
|
+
width: 0;
|
|
537
|
+
overflow: visible;
|
|
495
538
|
|
|
496
|
-
|
|
497
|
-
|
|
539
|
+
.header,
|
|
540
|
+
.nav,
|
|
541
|
+
.footer,
|
|
542
|
+
.rail {
|
|
543
|
+
visibility: hidden;
|
|
544
|
+
pointer-events: none;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.collapseToggleFloating {
|
|
548
|
+
visibility: visible;
|
|
549
|
+
pointer-events: auto;
|
|
498
550
|
}
|
|
499
551
|
}
|
|
500
552
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeAll
|
|
2
|
-
import { render, screen,
|
|
1
|
+
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
2
|
+
import { render, screen, expectNoA11yViolations } from '../../test/utils';
|
|
3
3
|
import { Sidebar } from './index';
|
|
4
4
|
|
|
5
5
|
// Mock matchMedia for jsdom
|
|
@@ -78,6 +78,35 @@ describe('Sidebar', () => {
|
|
|
78
78
|
expect(aside).toHaveAttribute('data-state');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it('uses icon collapse width when collapsed with icons', () => {
|
|
82
|
+
renderSidebar({ collapsed: true });
|
|
83
|
+
const aside = document.querySelector('aside');
|
|
84
|
+
expect(aside).toHaveStyle('--sidebar-effective-collapsed-width: 56px');
|
|
85
|
+
expect(aside).toHaveAttribute('data-icon-collapse', 'icons');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('collapses fully when collapsed with no item icons and keeps toggle visible', () => {
|
|
89
|
+
render(
|
|
90
|
+
<Sidebar collapsed aria-label="Text-only sidebar">
|
|
91
|
+
<Sidebar.Header>
|
|
92
|
+
Header Content
|
|
93
|
+
<Sidebar.CollapseToggle />
|
|
94
|
+
</Sidebar.Header>
|
|
95
|
+
<Sidebar.Nav aria-label="Main">
|
|
96
|
+
<Sidebar.Section label="Section One">
|
|
97
|
+
<Sidebar.Item>Dashboard</Sidebar.Item>
|
|
98
|
+
<Sidebar.Item active>Settings</Sidebar.Item>
|
|
99
|
+
</Sidebar.Section>
|
|
100
|
+
</Sidebar.Nav>
|
|
101
|
+
</Sidebar>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const aside = document.querySelector('aside');
|
|
105
|
+
expect(aside).toHaveStyle('--sidebar-effective-collapsed-width: 0px');
|
|
106
|
+
expect(aside).toHaveAttribute('data-icon-collapse', 'none');
|
|
107
|
+
expect(screen.getByRole('button', { name: /expand sidebar/i })).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
81
110
|
it('has no accessibility violations', async () => {
|
|
82
111
|
const { container } = renderSidebar();
|
|
83
112
|
await expectNoA11yViolations(container);
|