@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
@@ -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
- export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
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 defaultCollapsed={false}>
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 defaultCollapsed={false}>
715
+ <SidebarProvider>
694
716
  <Sidebar>
695
- {/* sidebar content */}
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-container-sm, $fui-padding-container-sm);
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-container-sm, $fui-padding-container-sm);
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-container-sm, $fui-padding-container-sm);
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: inline-flex;
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 accent-hover for active state to ensure WCAG AA contrast (4.5:1 minimum)
233
- background-color: var(--fui-color-accent-hover, $fui-color-accent-hover);
234
- color: var(--fui-text-inverse, $fui-text-inverse);
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-color-accent-hover, $fui-color-accent-hover) !important;
238
- color: var(--fui-text-inverse, $fui-text-inverse) !important;
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 (slides completely off-screen)
532
+ // Offcanvas Mode (collapses to zero width with floating toggle)
490
533
  // ============================================
491
534
 
492
- .offcanvas {
493
- transform: translateX(-100%);
494
- position: absolute;
535
+ .offcanvasCollapsed {
536
+ width: 0;
537
+ overflow: visible;
495
538
 
496
- &.positionRight {
497
- transform: translateX(100%);
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, afterAll } from 'vitest';
2
- import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
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);