@fragments-sdk/ui 0.2.2 → 0.3.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.
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Sidebar.module.scss';
|
|
3
|
+
import { Tooltip } from '../Tooltip';
|
|
4
|
+
import { Skeleton } from '../Skeleton';
|
|
5
|
+
// Import globals to ensure CSS variables are defined
|
|
6
|
+
import '../../styles/globals.scss';
|
|
7
|
+
|
|
8
|
+
// ============================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================
|
|
11
|
+
|
|
12
|
+
/** Collapse behavior mode */
|
|
13
|
+
export type SidebarCollapsible = 'icon' | 'offcanvas' | 'none';
|
|
14
|
+
|
|
15
|
+
export interface SidebarProviderProps {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
/** Icon-only mode (desktop) - controlled */
|
|
18
|
+
collapsed?: boolean;
|
|
19
|
+
/** Initial collapsed state (uncontrolled) */
|
|
20
|
+
defaultCollapsed?: boolean;
|
|
21
|
+
/** Callback when collapsed state changes */
|
|
22
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
23
|
+
/** Mobile drawer state - controlled */
|
|
24
|
+
open?: boolean;
|
|
25
|
+
/** Initial open state (uncontrolled) */
|
|
26
|
+
defaultOpen?: boolean;
|
|
27
|
+
/** Callback when open state changes */
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
/** Width of expanded sidebar */
|
|
30
|
+
width?: string;
|
|
31
|
+
/** Width when collapsed */
|
|
32
|
+
collapsedWidth?: string;
|
|
33
|
+
/** Sidebar position */
|
|
34
|
+
position?: 'left' | 'right';
|
|
35
|
+
/** Collapse behavior: 'icon' (default), 'offcanvas', or 'none' */
|
|
36
|
+
collapsible?: SidebarCollapsible;
|
|
37
|
+
/** Enable Cmd/Ctrl+B keyboard shortcut to toggle sidebar */
|
|
38
|
+
enableKeyboardShortcut?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SidebarProps {
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
/** Icon-only mode (desktop) - controlled */
|
|
44
|
+
collapsed?: boolean;
|
|
45
|
+
/** Initial collapsed state (uncontrolled) */
|
|
46
|
+
defaultCollapsed?: boolean;
|
|
47
|
+
/** Callback when collapsed state changes */
|
|
48
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
49
|
+
/** Mobile drawer state - controlled */
|
|
50
|
+
open?: boolean;
|
|
51
|
+
/** Initial open state (uncontrolled) */
|
|
52
|
+
defaultOpen?: boolean;
|
|
53
|
+
/** Callback when open state changes */
|
|
54
|
+
onOpenChange?: (open: boolean) => void;
|
|
55
|
+
/** Width of expanded sidebar */
|
|
56
|
+
width?: string;
|
|
57
|
+
/** Width when collapsed */
|
|
58
|
+
collapsedWidth?: string;
|
|
59
|
+
/** Sidebar position */
|
|
60
|
+
position?: 'left' | 'right';
|
|
61
|
+
/** Collapse behavior: 'icon' (default), 'offcanvas', or 'none' */
|
|
62
|
+
collapsible?: SidebarCollapsible;
|
|
63
|
+
/** Additional class name */
|
|
64
|
+
className?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SidebarHeaderProps {
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
/** Content to show when sidebar is collapsed (e.g., just logo icon) */
|
|
70
|
+
collapsedContent?: React.ReactNode;
|
|
71
|
+
className?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SidebarNavProps {
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
/** Accessible label for navigation */
|
|
77
|
+
'aria-label'?: string;
|
|
78
|
+
className?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SidebarSectionProps {
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
/** Optional section label */
|
|
84
|
+
label?: string;
|
|
85
|
+
/** Action element to display in the section header (e.g., "Add" button) */
|
|
86
|
+
action?: React.ReactNode;
|
|
87
|
+
className?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SidebarSectionActionProps {
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
/** Click handler */
|
|
93
|
+
onClick?: () => void;
|
|
94
|
+
/** Accessible label */
|
|
95
|
+
'aria-label'?: string;
|
|
96
|
+
className?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SidebarItemProps {
|
|
100
|
+
children: React.ReactNode;
|
|
101
|
+
/** Icon element (required for collapsed mode visibility) */
|
|
102
|
+
icon?: React.ReactNode;
|
|
103
|
+
/** Whether item is currently active/selected */
|
|
104
|
+
active?: boolean;
|
|
105
|
+
/** Disabled state */
|
|
106
|
+
disabled?: boolean;
|
|
107
|
+
/** Badge content (e.g., notification count) */
|
|
108
|
+
badge?: React.ReactNode;
|
|
109
|
+
/** Renders as anchor if provided */
|
|
110
|
+
href?: string;
|
|
111
|
+
/** Click handler (renders as button) */
|
|
112
|
+
onClick?: () => void;
|
|
113
|
+
/** Whether this item has a submenu */
|
|
114
|
+
hasSubmenu?: boolean;
|
|
115
|
+
/** Whether submenu is expanded (controlled) */
|
|
116
|
+
expanded?: boolean;
|
|
117
|
+
/** Initial expanded state (uncontrolled) */
|
|
118
|
+
defaultExpanded?: boolean;
|
|
119
|
+
/** Callback when expanded state changes */
|
|
120
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
121
|
+
/**
|
|
122
|
+
* Render as child element (polymorphic). When true, clones the single child
|
|
123
|
+
* and merges sidebar item props. Useful for rendering as Next.js Link, etc.
|
|
124
|
+
*/
|
|
125
|
+
asChild?: boolean;
|
|
126
|
+
className?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SidebarSubItemProps {
|
|
130
|
+
children: React.ReactNode;
|
|
131
|
+
/** Whether item is currently active/selected */
|
|
132
|
+
active?: boolean;
|
|
133
|
+
/** Disabled state */
|
|
134
|
+
disabled?: boolean;
|
|
135
|
+
/** Renders as anchor if provided */
|
|
136
|
+
href?: string;
|
|
137
|
+
/** Click handler (renders as button) */
|
|
138
|
+
onClick?: () => void;
|
|
139
|
+
className?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SidebarFooterProps {
|
|
143
|
+
children: React.ReactNode;
|
|
144
|
+
className?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface SidebarTriggerProps {
|
|
148
|
+
/** Custom trigger element (uses render prop pattern) */
|
|
149
|
+
children?: React.ReactNode;
|
|
150
|
+
/** Accessible label */
|
|
151
|
+
'aria-label'?: string;
|
|
152
|
+
className?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SidebarOverlayProps {
|
|
156
|
+
className?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface SidebarCollapseToggleProps {
|
|
160
|
+
/** Accessible label */
|
|
161
|
+
'aria-label'?: string;
|
|
162
|
+
className?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface SidebarRailProps {
|
|
166
|
+
className?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SidebarMenuSkeletonProps {
|
|
170
|
+
/** Number of skeleton items to render */
|
|
171
|
+
count?: number;
|
|
172
|
+
/** Show icons in skeleton items */
|
|
173
|
+
showIcon?: boolean;
|
|
174
|
+
className?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================
|
|
178
|
+
// Icons
|
|
179
|
+
// ============================================
|
|
180
|
+
|
|
181
|
+
function MenuIcon() {
|
|
182
|
+
return (
|
|
183
|
+
<svg
|
|
184
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
185
|
+
width="24"
|
|
186
|
+
height="24"
|
|
187
|
+
viewBox="0 0 256 256"
|
|
188
|
+
fill="currentColor"
|
|
189
|
+
aria-hidden="true"
|
|
190
|
+
>
|
|
191
|
+
<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" />
|
|
192
|
+
</svg>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function CloseIcon() {
|
|
197
|
+
return (
|
|
198
|
+
<svg
|
|
199
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
200
|
+
width="24"
|
|
201
|
+
height="24"
|
|
202
|
+
viewBox="0 0 256 256"
|
|
203
|
+
fill="currentColor"
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
>
|
|
206
|
+
<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" />
|
|
207
|
+
</svg>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function CollapseLeftIcon() {
|
|
212
|
+
return (
|
|
213
|
+
<svg
|
|
214
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
215
|
+
width="20"
|
|
216
|
+
height="20"
|
|
217
|
+
viewBox="0 0 256 256"
|
|
218
|
+
fill="currentColor"
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
>
|
|
221
|
+
<path d="M141.66,181.66a8,8,0,0,1-11.32,0l-48-48a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,11.32L99.31,128l42.35,42.34A8,8,0,0,1,141.66,181.66Zm40-96L139.31,128l42.35,42.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48a8,8,0,0,1,11.32,11.32Z" />
|
|
222
|
+
</svg>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function CollapseRightIcon() {
|
|
227
|
+
return (
|
|
228
|
+
<svg
|
|
229
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
230
|
+
width="20"
|
|
231
|
+
height="20"
|
|
232
|
+
viewBox="0 0 256 256"
|
|
233
|
+
fill="currentColor"
|
|
234
|
+
aria-hidden="true"
|
|
235
|
+
>
|
|
236
|
+
<path d="M141.66,133.66l-48,48a8,8,0,0,1-11.32-11.32L124.69,128,82.34,85.66a8,8,0,0,1,11.32-11.32l48,48A8,8,0,0,1,141.66,133.66Zm40-11.32-48-48a8,8,0,0,0-11.32,11.32L164.69,128l-42.35,42.34a8,8,0,0,0,11.32,11.32l48-48A8,8,0,0,0,181.66,122.34Z" />
|
|
237
|
+
</svg>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ChevronRightIcon() {
|
|
242
|
+
return (
|
|
243
|
+
<svg
|
|
244
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
245
|
+
width="16"
|
|
246
|
+
height="16"
|
|
247
|
+
viewBox="0 0 256 256"
|
|
248
|
+
fill="currentColor"
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
>
|
|
251
|
+
<path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z" />
|
|
252
|
+
</svg>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================
|
|
257
|
+
// Context
|
|
258
|
+
// ============================================
|
|
259
|
+
|
|
260
|
+
interface SidebarContextValue {
|
|
261
|
+
collapsed: boolean;
|
|
262
|
+
setCollapsed: (collapsed: boolean) => void;
|
|
263
|
+
open: boolean;
|
|
264
|
+
setOpen: (open: boolean) => void;
|
|
265
|
+
isMobile: boolean;
|
|
266
|
+
position: 'left' | 'right';
|
|
267
|
+
width: string;
|
|
268
|
+
collapsedWidth: string;
|
|
269
|
+
collapsible: SidebarCollapsible;
|
|
270
|
+
toggleSidebar: () => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const SidebarContext = React.createContext<SidebarContextValue | null>(null);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Hook to access sidebar state and controls.
|
|
277
|
+
* Returns safe defaults if used outside a SidebarProvider/Sidebar.
|
|
278
|
+
*/
|
|
279
|
+
function useSidebar() {
|
|
280
|
+
const context = React.useContext(SidebarContext);
|
|
281
|
+
if (!context) {
|
|
282
|
+
// Return safe defaults when used outside provider
|
|
283
|
+
return {
|
|
284
|
+
collapsed: false,
|
|
285
|
+
setCollapsed: () => {},
|
|
286
|
+
open: false,
|
|
287
|
+
setOpen: () => {},
|
|
288
|
+
isMobile: false,
|
|
289
|
+
position: 'left' as const,
|
|
290
|
+
width: '240px',
|
|
291
|
+
collapsedWidth: '64px',
|
|
292
|
+
collapsible: 'icon' as SidebarCollapsible,
|
|
293
|
+
toggleSidebar: () => {},
|
|
294
|
+
state: 'expanded' as 'expanded' | 'collapsed' | 'open' | 'closed',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
...context,
|
|
299
|
+
state: context.isMobile
|
|
300
|
+
? (context.open ? 'open' : 'closed')
|
|
301
|
+
: (context.collapsed ? 'collapsed' : 'expanded'),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @deprecated Use `useSidebar` instead. This will be removed in a future version.
|
|
307
|
+
*/
|
|
308
|
+
function useSidebarContext() {
|
|
309
|
+
const context = React.useContext(SidebarContext);
|
|
310
|
+
if (!context) {
|
|
311
|
+
throw new Error('Sidebar compound components must be used within a Sidebar');
|
|
312
|
+
}
|
|
313
|
+
return context;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================
|
|
317
|
+
// Hooks
|
|
318
|
+
// ============================================
|
|
319
|
+
|
|
320
|
+
function useIsMobile() {
|
|
321
|
+
const [isMobile, setIsMobile] = React.useState(false);
|
|
322
|
+
|
|
323
|
+
React.useEffect(() => {
|
|
324
|
+
const mq = window.matchMedia('(max-width: 767px)');
|
|
325
|
+
setIsMobile(mq.matches);
|
|
326
|
+
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
|
327
|
+
mq.addEventListener('change', handler);
|
|
328
|
+
return () => mq.removeEventListener('change', handler);
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
return isMobile;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function useControllableState<T>(
|
|
335
|
+
controlledValue: T | undefined,
|
|
336
|
+
defaultValue: T,
|
|
337
|
+
onChange?: (value: T) => void
|
|
338
|
+
): [T, (value: T) => void] {
|
|
339
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
|
|
340
|
+
const isControlled = controlledValue !== undefined;
|
|
341
|
+
const value = isControlled ? controlledValue : uncontrolledValue;
|
|
342
|
+
|
|
343
|
+
const setValue = React.useCallback(
|
|
344
|
+
(newValue: T) => {
|
|
345
|
+
if (!isControlled) {
|
|
346
|
+
setUncontrolledValue(newValue);
|
|
347
|
+
}
|
|
348
|
+
onChange?.(newValue);
|
|
349
|
+
},
|
|
350
|
+
[isControlled, onChange]
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return [value, setValue];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================
|
|
357
|
+
// Components
|
|
358
|
+
// ============================================
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* SidebarProvider - Wrap your app layout to provide sidebar state to children.
|
|
362
|
+
* This enables external triggers and keyboard shortcuts.
|
|
363
|
+
*/
|
|
364
|
+
function SidebarProvider({
|
|
365
|
+
children,
|
|
366
|
+
collapsed: controlledCollapsed,
|
|
367
|
+
defaultCollapsed = false,
|
|
368
|
+
onCollapsedChange,
|
|
369
|
+
open: controlledOpen,
|
|
370
|
+
defaultOpen = false,
|
|
371
|
+
onOpenChange,
|
|
372
|
+
width = '240px',
|
|
373
|
+
collapsedWidth = '64px',
|
|
374
|
+
position = 'left',
|
|
375
|
+
collapsible = 'icon',
|
|
376
|
+
enableKeyboardShortcut = true,
|
|
377
|
+
}: SidebarProviderProps) {
|
|
378
|
+
const isMobile = useIsMobile();
|
|
379
|
+
|
|
380
|
+
const [collapsed, setCollapsed] = useControllableState(
|
|
381
|
+
controlledCollapsed,
|
|
382
|
+
defaultCollapsed,
|
|
383
|
+
onCollapsedChange
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const [open, setOpen] = useControllableState(
|
|
387
|
+
controlledOpen,
|
|
388
|
+
defaultOpen,
|
|
389
|
+
onOpenChange
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const toggleSidebar = React.useCallback(() => {
|
|
393
|
+
if (collapsible === 'none') return;
|
|
394
|
+
if (isMobile) {
|
|
395
|
+
setOpen(!open);
|
|
396
|
+
} else {
|
|
397
|
+
setCollapsed(!collapsed);
|
|
398
|
+
}
|
|
399
|
+
}, [isMobile, open, collapsed, setOpen, setCollapsed, collapsible]);
|
|
400
|
+
|
|
401
|
+
// Handle Cmd/Ctrl+B keyboard shortcut
|
|
402
|
+
React.useEffect(() => {
|
|
403
|
+
if (!enableKeyboardShortcut || collapsible === 'none') return;
|
|
404
|
+
|
|
405
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
406
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
toggleSidebar();
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
413
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
414
|
+
}, [enableKeyboardShortcut, toggleSidebar, collapsible]);
|
|
415
|
+
|
|
416
|
+
// Handle escape key for mobile drawer
|
|
417
|
+
React.useEffect(() => {
|
|
418
|
+
if (!isMobile || !open) return;
|
|
419
|
+
|
|
420
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
421
|
+
if (e.key === 'Escape') {
|
|
422
|
+
setOpen(false);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
document.addEventListener('keydown', handleEscape);
|
|
427
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
428
|
+
}, [isMobile, open, setOpen]);
|
|
429
|
+
|
|
430
|
+
// Lock body scroll when mobile drawer is open
|
|
431
|
+
React.useEffect(() => {
|
|
432
|
+
if (!isMobile) return;
|
|
433
|
+
|
|
434
|
+
if (open) {
|
|
435
|
+
document.body.style.overflow = 'hidden';
|
|
436
|
+
} else {
|
|
437
|
+
document.body.style.overflow = '';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return () => {
|
|
441
|
+
document.body.style.overflow = '';
|
|
442
|
+
};
|
|
443
|
+
}, [isMobile, open]);
|
|
444
|
+
|
|
445
|
+
const contextValue: SidebarContextValue = {
|
|
446
|
+
collapsed,
|
|
447
|
+
setCollapsed,
|
|
448
|
+
open,
|
|
449
|
+
setOpen,
|
|
450
|
+
isMobile,
|
|
451
|
+
position,
|
|
452
|
+
width,
|
|
453
|
+
collapsedWidth,
|
|
454
|
+
collapsible,
|
|
455
|
+
toggleSidebar,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<SidebarContext.Provider value={contextValue}>
|
|
460
|
+
{children}
|
|
461
|
+
</SidebarContext.Provider>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function SidebarRoot({
|
|
466
|
+
children,
|
|
467
|
+
collapsed: controlledCollapsed,
|
|
468
|
+
defaultCollapsed = false,
|
|
469
|
+
onCollapsedChange,
|
|
470
|
+
open: controlledOpen,
|
|
471
|
+
defaultOpen = false,
|
|
472
|
+
onOpenChange,
|
|
473
|
+
width = '240px',
|
|
474
|
+
collapsedWidth = '64px',
|
|
475
|
+
position = 'left',
|
|
476
|
+
collapsible = 'icon',
|
|
477
|
+
className,
|
|
478
|
+
}: SidebarProps) {
|
|
479
|
+
// Check if we're inside a SidebarProvider
|
|
480
|
+
const existingContext = React.useContext(SidebarContext);
|
|
481
|
+
const isMobile = useIsMobile();
|
|
482
|
+
|
|
483
|
+
const [internalCollapsed, setInternalCollapsed] = useControllableState(
|
|
484
|
+
controlledCollapsed,
|
|
485
|
+
defaultCollapsed,
|
|
486
|
+
onCollapsedChange
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const [internalOpen, setInternalOpen] = useControllableState(
|
|
490
|
+
controlledOpen,
|
|
491
|
+
defaultOpen,
|
|
492
|
+
onOpenChange
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Use existing context values if inside a provider, otherwise use internal state
|
|
496
|
+
const collapsed = existingContext ? existingContext.collapsed : internalCollapsed;
|
|
497
|
+
const setCollapsed = existingContext ? existingContext.setCollapsed : setInternalCollapsed;
|
|
498
|
+
const open = existingContext ? existingContext.open : internalOpen;
|
|
499
|
+
const setOpen = existingContext ? existingContext.setOpen : setInternalOpen;
|
|
500
|
+
const resolvedPosition = existingContext ? existingContext.position : position;
|
|
501
|
+
const resolvedWidth = existingContext ? existingContext.width : width;
|
|
502
|
+
const resolvedCollapsedWidth = existingContext ? existingContext.collapsedWidth : collapsedWidth;
|
|
503
|
+
const resolvedCollapsible = existingContext ? existingContext.collapsible : collapsible;
|
|
504
|
+
|
|
505
|
+
const toggleSidebar = React.useCallback(() => {
|
|
506
|
+
if (resolvedCollapsible === 'none') return;
|
|
507
|
+
if (isMobile) {
|
|
508
|
+
setOpen(!open);
|
|
509
|
+
} else {
|
|
510
|
+
setCollapsed(!collapsed);
|
|
511
|
+
}
|
|
512
|
+
}, [isMobile, open, collapsed, setOpen, setCollapsed, resolvedCollapsible]);
|
|
513
|
+
|
|
514
|
+
// Handle escape key for mobile drawer (only if no provider)
|
|
515
|
+
React.useEffect(() => {
|
|
516
|
+
if (existingContext) return; // Provider handles this
|
|
517
|
+
if (!isMobile || !open) return;
|
|
518
|
+
|
|
519
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
520
|
+
if (e.key === 'Escape') {
|
|
521
|
+
setOpen(false);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
document.addEventListener('keydown', handleEscape);
|
|
526
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
527
|
+
}, [existingContext, isMobile, open, setOpen]);
|
|
528
|
+
|
|
529
|
+
// Lock body scroll when mobile drawer is open (only if no provider)
|
|
530
|
+
React.useEffect(() => {
|
|
531
|
+
if (existingContext) return; // Provider handles this
|
|
532
|
+
if (!isMobile) return;
|
|
533
|
+
|
|
534
|
+
if (open) {
|
|
535
|
+
document.body.style.overflow = 'hidden';
|
|
536
|
+
} else {
|
|
537
|
+
document.body.style.overflow = '';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return () => {
|
|
541
|
+
document.body.style.overflow = '';
|
|
542
|
+
};
|
|
543
|
+
}, [existingContext, isMobile, open]);
|
|
544
|
+
|
|
545
|
+
const contextValue: SidebarContextValue = existingContext || {
|
|
546
|
+
collapsed,
|
|
547
|
+
setCollapsed,
|
|
548
|
+
open,
|
|
549
|
+
setOpen,
|
|
550
|
+
isMobile,
|
|
551
|
+
position: resolvedPosition,
|
|
552
|
+
width: resolvedWidth,
|
|
553
|
+
collapsedWidth: resolvedCollapsedWidth,
|
|
554
|
+
collapsible: resolvedCollapsible,
|
|
555
|
+
toggleSidebar,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const isCollapsedForStyle = resolvedCollapsible === 'icon' && collapsed;
|
|
559
|
+
const isOffcanvas = resolvedCollapsible === 'offcanvas' && collapsed;
|
|
560
|
+
|
|
561
|
+
const classes = [
|
|
562
|
+
styles.root,
|
|
563
|
+
isMobile && styles.mobile,
|
|
564
|
+
!isMobile && isCollapsedForStyle && styles.collapsed,
|
|
565
|
+
!isMobile && isOffcanvas && styles.offcanvas,
|
|
566
|
+
resolvedPosition === 'right' && styles.positionRight,
|
|
567
|
+
className,
|
|
568
|
+
].filter(Boolean).join(' ');
|
|
569
|
+
|
|
570
|
+
const style: React.CSSProperties = {
|
|
571
|
+
'--sidebar-width': resolvedWidth,
|
|
572
|
+
'--sidebar-collapsed-width': resolvedCollapsedWidth,
|
|
573
|
+
} as React.CSSProperties;
|
|
574
|
+
|
|
575
|
+
const content = (
|
|
576
|
+
<aside
|
|
577
|
+
className={classes}
|
|
578
|
+
style={style}
|
|
579
|
+
data-state={isMobile ? (open ? 'open' : 'closed') : (collapsed ? 'collapsed' : 'expanded')}
|
|
580
|
+
data-position={resolvedPosition}
|
|
581
|
+
data-collapsible={resolvedCollapsible}
|
|
582
|
+
>
|
|
583
|
+
{children}
|
|
584
|
+
</aside>
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// If already inside a provider, don't wrap with another provider
|
|
588
|
+
if (existingContext) {
|
|
589
|
+
return content;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<SidebarContext.Provider value={contextValue}>
|
|
594
|
+
{content}
|
|
595
|
+
</SidebarContext.Provider>
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function SidebarHeader({ children, collapsedContent, className }: SidebarHeaderProps) {
|
|
600
|
+
const { collapsed, isMobile } = useSidebarContext();
|
|
601
|
+
const isCollapsed = collapsed && !isMobile;
|
|
602
|
+
const classes = [styles.header, className].filter(Boolean).join(' ');
|
|
603
|
+
|
|
604
|
+
// Show collapsed content when sidebar is collapsed (and we have it), otherwise show children
|
|
605
|
+
const content = isCollapsed && collapsedContent ? collapsedContent : children;
|
|
606
|
+
|
|
607
|
+
return <div className={classes}>{content}</div>;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function SidebarNav({ children, 'aria-label': ariaLabel = 'Main navigation', className }: SidebarNavProps) {
|
|
611
|
+
const classes = [styles.nav, className].filter(Boolean).join(' ');
|
|
612
|
+
return (
|
|
613
|
+
<nav className={classes} aria-label={ariaLabel}>
|
|
614
|
+
{children}
|
|
615
|
+
</nav>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function SidebarSection({ children, label, action, className }: SidebarSectionProps) {
|
|
620
|
+
const { collapsed, isMobile } = useSidebarContext();
|
|
621
|
+
const classes = [styles.section, className].filter(Boolean).join(' ');
|
|
622
|
+
const showLabel = label && (!collapsed || isMobile);
|
|
623
|
+
const showAction = action && (!collapsed || isMobile);
|
|
624
|
+
|
|
625
|
+
return (
|
|
626
|
+
<div className={classes} role="group" aria-label={label}>
|
|
627
|
+
{(showLabel || showAction) && (
|
|
628
|
+
<div className={styles.sectionHeader}>
|
|
629
|
+
{showLabel && <div className={styles.sectionLabel}>{label}</div>}
|
|
630
|
+
{showAction && <div className={styles.sectionActionWrapper}>{action}</div>}
|
|
631
|
+
</div>
|
|
632
|
+
)}
|
|
633
|
+
<ul className={styles.sectionList}>
|
|
634
|
+
{children}
|
|
635
|
+
</ul>
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function SidebarSectionAction({
|
|
641
|
+
children,
|
|
642
|
+
onClick,
|
|
643
|
+
'aria-label': ariaLabel,
|
|
644
|
+
className,
|
|
645
|
+
}: SidebarSectionActionProps) {
|
|
646
|
+
const classes = [styles.sectionAction, className].filter(Boolean).join(' ');
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
<button
|
|
650
|
+
type="button"
|
|
651
|
+
className={classes}
|
|
652
|
+
onClick={onClick}
|
|
653
|
+
aria-label={ariaLabel}
|
|
654
|
+
>
|
|
655
|
+
{children}
|
|
656
|
+
</button>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function SidebarItem({
|
|
661
|
+
children,
|
|
662
|
+
icon,
|
|
663
|
+
active = false,
|
|
664
|
+
disabled = false,
|
|
665
|
+
badge,
|
|
666
|
+
href,
|
|
667
|
+
onClick,
|
|
668
|
+
hasSubmenu = false,
|
|
669
|
+
expanded: controlledExpanded,
|
|
670
|
+
defaultExpanded = false,
|
|
671
|
+
onExpandedChange,
|
|
672
|
+
asChild = false,
|
|
673
|
+
className,
|
|
674
|
+
}: SidebarItemProps) {
|
|
675
|
+
const { collapsed, isMobile } = useSidebarContext();
|
|
676
|
+
const [expanded, setExpanded] = useControllableState(
|
|
677
|
+
controlledExpanded,
|
|
678
|
+
defaultExpanded,
|
|
679
|
+
onExpandedChange
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
const isCollapsed = collapsed && !isMobile;
|
|
683
|
+
const showLabel = !isCollapsed;
|
|
684
|
+
|
|
685
|
+
const classes = [
|
|
686
|
+
styles.item,
|
|
687
|
+
active && styles.itemActive,
|
|
688
|
+
disabled && styles.itemDisabled,
|
|
689
|
+
hasSubmenu && styles.itemHasSubmenu,
|
|
690
|
+
expanded && styles.itemExpanded,
|
|
691
|
+
className,
|
|
692
|
+
].filter(Boolean).join(' ');
|
|
693
|
+
|
|
694
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
695
|
+
if (disabled) {
|
|
696
|
+
e.preventDefault();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (hasSubmenu) {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
setExpanded(!expanded);
|
|
702
|
+
}
|
|
703
|
+
onClick?.();
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Extract text content from children for aria-label when collapsed
|
|
707
|
+
// For asChild, try to extract from the child's children prop
|
|
708
|
+
let labelText: string | undefined;
|
|
709
|
+
if (asChild && React.isValidElement(children)) {
|
|
710
|
+
const childProps = children.props as { children?: React.ReactNode };
|
|
711
|
+
labelText = typeof childProps.children === 'string' ? childProps.children : undefined;
|
|
712
|
+
} else {
|
|
713
|
+
labelText = typeof children === 'string' ? children : undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const itemContent = (
|
|
717
|
+
<>
|
|
718
|
+
{icon && <span className={styles.itemIcon}>{icon}</span>}
|
|
719
|
+
{showLabel && <span className={styles.itemLabel}>{asChild && React.isValidElement(children) ? (children.props as { children?: React.ReactNode }).children : children}</span>}
|
|
720
|
+
{showLabel && badge && <span className={styles.itemBadge}>{badge}</span>}
|
|
721
|
+
{showLabel && hasSubmenu && (
|
|
722
|
+
<span className={styles.itemChevron}>
|
|
723
|
+
<ChevronRightIcon />
|
|
724
|
+
</span>
|
|
725
|
+
)}
|
|
726
|
+
</>
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
const itemProps = {
|
|
730
|
+
className: classes,
|
|
731
|
+
onClick: handleClick,
|
|
732
|
+
'aria-current': active ? 'page' as const : undefined,
|
|
733
|
+
'aria-disabled': disabled || undefined,
|
|
734
|
+
'aria-expanded': hasSubmenu ? expanded : undefined,
|
|
735
|
+
'aria-label': isCollapsed ? labelText : undefined,
|
|
736
|
+
tabIndex: disabled ? -1 : 0,
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
let itemElement: React.ReactElement;
|
|
740
|
+
|
|
741
|
+
if (asChild && React.isValidElement(children)) {
|
|
742
|
+
// Clone the child element and merge props
|
|
743
|
+
itemElement = React.cloneElement(children, {
|
|
744
|
+
...itemProps,
|
|
745
|
+
// Merge classNames
|
|
746
|
+
className: [classes, (children.props as { className?: string }).className].filter(Boolean).join(' '),
|
|
747
|
+
children: itemContent,
|
|
748
|
+
} as React.HTMLAttributes<HTMLElement>);
|
|
749
|
+
} else if (href) {
|
|
750
|
+
itemElement = (
|
|
751
|
+
<a {...itemProps} href={href}>
|
|
752
|
+
{itemContent}
|
|
753
|
+
</a>
|
|
754
|
+
);
|
|
755
|
+
} else {
|
|
756
|
+
itemElement = (
|
|
757
|
+
<button {...itemProps} type="button">
|
|
758
|
+
{itemContent}
|
|
759
|
+
</button>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Wrap in tooltip when collapsed
|
|
764
|
+
const wrappedItem = isCollapsed ? (
|
|
765
|
+
<Tooltip content={labelText || children} side="right" delay={100}>
|
|
766
|
+
{itemElement}
|
|
767
|
+
</Tooltip>
|
|
768
|
+
) : (
|
|
769
|
+
itemElement
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
const wrapperClasses = [
|
|
773
|
+
styles.itemWrapper,
|
|
774
|
+
expanded && styles.itemExpanded,
|
|
775
|
+
].filter(Boolean).join(' ');
|
|
776
|
+
|
|
777
|
+
return <li className={wrapperClasses}>{wrappedItem}</li>;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function SidebarSubItem({
|
|
781
|
+
children,
|
|
782
|
+
active = false,
|
|
783
|
+
disabled = false,
|
|
784
|
+
href,
|
|
785
|
+
onClick,
|
|
786
|
+
className,
|
|
787
|
+
}: SidebarSubItemProps) {
|
|
788
|
+
const { collapsed, isMobile } = useSidebarContext();
|
|
789
|
+
|
|
790
|
+
// Don't render sub-items when collapsed (unless mobile)
|
|
791
|
+
if (collapsed && !isMobile) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const classes = [
|
|
796
|
+
styles.subItem,
|
|
797
|
+
active && styles.subItemActive,
|
|
798
|
+
disabled && styles.subItemDisabled,
|
|
799
|
+
className,
|
|
800
|
+
].filter(Boolean).join(' ');
|
|
801
|
+
|
|
802
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
803
|
+
if (disabled) {
|
|
804
|
+
e.preventDefault();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
onClick?.();
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const itemProps = {
|
|
811
|
+
className: classes,
|
|
812
|
+
onClick: handleClick,
|
|
813
|
+
'aria-current': active ? 'page' as const : undefined,
|
|
814
|
+
'aria-disabled': disabled,
|
|
815
|
+
tabIndex: disabled ? -1 : 0,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const itemElement = href ? (
|
|
819
|
+
<a {...itemProps} href={href}>
|
|
820
|
+
{children}
|
|
821
|
+
</a>
|
|
822
|
+
) : (
|
|
823
|
+
<button {...itemProps} type="button">
|
|
824
|
+
{children}
|
|
825
|
+
</button>
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
return <li className={styles.subItemWrapper}>{itemElement}</li>;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function SidebarSubmenu({ children }: { children: React.ReactNode }) {
|
|
832
|
+
return (
|
|
833
|
+
<li className={styles.submenuWrapper}>
|
|
834
|
+
<ul className={styles.submenu}>
|
|
835
|
+
{children}
|
|
836
|
+
</ul>
|
|
837
|
+
</li>
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function SidebarFooter({ children, className }: SidebarFooterProps) {
|
|
842
|
+
const classes = [styles.footer, className].filter(Boolean).join(' ');
|
|
843
|
+
return <div className={classes}>{children}</div>;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function SidebarTrigger({ children, 'aria-label': ariaLabel = 'Toggle navigation', className }: SidebarTriggerProps) {
|
|
847
|
+
const { open, setOpen, isMobile } = useSidebarContext();
|
|
848
|
+
|
|
849
|
+
// Only render trigger on mobile
|
|
850
|
+
if (!isMobile) {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const classes = [styles.trigger, className].filter(Boolean).join(' ');
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<button
|
|
858
|
+
type="button"
|
|
859
|
+
className={classes}
|
|
860
|
+
onClick={() => setOpen(!open)}
|
|
861
|
+
aria-label={ariaLabel}
|
|
862
|
+
aria-expanded={open}
|
|
863
|
+
>
|
|
864
|
+
{children || (open ? <CloseIcon /> : <MenuIcon />)}
|
|
865
|
+
</button>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function SidebarOverlay({ className }: SidebarOverlayProps) {
|
|
870
|
+
const { open, setOpen, isMobile } = useSidebarContext();
|
|
871
|
+
|
|
872
|
+
// Only render overlay on mobile when open
|
|
873
|
+
if (!isMobile || !open) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const classes = [styles.overlay, className].filter(Boolean).join(' ');
|
|
878
|
+
|
|
879
|
+
return (
|
|
880
|
+
<div
|
|
881
|
+
className={classes}
|
|
882
|
+
onClick={() => setOpen(false)}
|
|
883
|
+
aria-hidden="true"
|
|
884
|
+
data-state={open ? 'open' : 'closed'}
|
|
885
|
+
/>
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function SidebarCollapseToggle({ 'aria-label': ariaLabel, className }: SidebarCollapseToggleProps) {
|
|
890
|
+
const { collapsed, setCollapsed, isMobile, position, collapsible } = useSidebarContext();
|
|
891
|
+
|
|
892
|
+
// Don't show on mobile or when collapsing is disabled
|
|
893
|
+
if (isMobile || collapsible === 'none') {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const classes = [styles.collapseToggle, className].filter(Boolean).join(' ');
|
|
898
|
+
const label = ariaLabel || (collapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
899
|
+
|
|
900
|
+
// Determine which icon to show based on position and state
|
|
901
|
+
const showExpandIcon = position === 'left' ? collapsed : !collapsed;
|
|
902
|
+
|
|
903
|
+
return (
|
|
904
|
+
<button
|
|
905
|
+
type="button"
|
|
906
|
+
className={classes}
|
|
907
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
908
|
+
aria-label={label}
|
|
909
|
+
>
|
|
910
|
+
{showExpandIcon ? <CollapseRightIcon /> : <CollapseLeftIcon />}
|
|
911
|
+
</button>
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function SidebarRail({ className }: SidebarRailProps) {
|
|
916
|
+
const { collapsed, setCollapsed, isMobile, collapsible } = useSidebarContext();
|
|
917
|
+
|
|
918
|
+
// Don't show on mobile or when collapsing is disabled
|
|
919
|
+
if (isMobile || collapsible === 'none') {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const classes = [
|
|
924
|
+
styles.rail,
|
|
925
|
+
collapsed && styles.railCollapsed,
|
|
926
|
+
className,
|
|
927
|
+
].filter(Boolean).join(' ');
|
|
928
|
+
|
|
929
|
+
return (
|
|
930
|
+
<button
|
|
931
|
+
type="button"
|
|
932
|
+
className={classes}
|
|
933
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
934
|
+
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
935
|
+
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
936
|
+
/>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function SidebarMenuSkeleton({
|
|
941
|
+
count = 5,
|
|
942
|
+
showIcon = true,
|
|
943
|
+
className,
|
|
944
|
+
}: SidebarMenuSkeletonProps) {
|
|
945
|
+
const { collapsed, isMobile } = useSidebarContext();
|
|
946
|
+
const isCollapsed = collapsed && !isMobile;
|
|
947
|
+
|
|
948
|
+
const classes = [styles.menuSkeleton, className].filter(Boolean).join(' ');
|
|
949
|
+
|
|
950
|
+
return (
|
|
951
|
+
<div className={classes} aria-hidden="true">
|
|
952
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
953
|
+
<div key={i} className={styles.skeletonItem}>
|
|
954
|
+
{showIcon && <Skeleton variant="avatar" size="sm" />}
|
|
955
|
+
{!isCollapsed && (
|
|
956
|
+
<Skeleton
|
|
957
|
+
variant="text"
|
|
958
|
+
className={styles.skeletonLabel}
|
|
959
|
+
width={`${60 + Math.random() * 30}%`}
|
|
960
|
+
/>
|
|
961
|
+
)}
|
|
962
|
+
</div>
|
|
963
|
+
))}
|
|
964
|
+
</div>
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ============================================
|
|
969
|
+
// Export compound component
|
|
970
|
+
// ============================================
|
|
971
|
+
|
|
972
|
+
export const Sidebar = Object.assign(SidebarRoot, {
|
|
973
|
+
Header: SidebarHeader,
|
|
974
|
+
Nav: SidebarNav,
|
|
975
|
+
Section: SidebarSection,
|
|
976
|
+
SectionAction: SidebarSectionAction,
|
|
977
|
+
Item: SidebarItem,
|
|
978
|
+
SubItem: SidebarSubItem,
|
|
979
|
+
Submenu: SidebarSubmenu,
|
|
980
|
+
Footer: SidebarFooter,
|
|
981
|
+
Trigger: SidebarTrigger,
|
|
982
|
+
Overlay: SidebarOverlay,
|
|
983
|
+
CollapseToggle: SidebarCollapseToggle,
|
|
984
|
+
Rail: SidebarRail,
|
|
985
|
+
MenuSkeleton: SidebarMenuSkeleton,
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Re-export individual components
|
|
989
|
+
export {
|
|
990
|
+
SidebarProvider,
|
|
991
|
+
SidebarRoot,
|
|
992
|
+
SidebarHeader,
|
|
993
|
+
SidebarNav,
|
|
994
|
+
SidebarSection,
|
|
995
|
+
SidebarSectionAction,
|
|
996
|
+
SidebarItem,
|
|
997
|
+
SidebarSubItem,
|
|
998
|
+
SidebarSubmenu,
|
|
999
|
+
SidebarFooter,
|
|
1000
|
+
SidebarTrigger,
|
|
1001
|
+
SidebarOverlay,
|
|
1002
|
+
SidebarCollapseToggle,
|
|
1003
|
+
SidebarRail,
|
|
1004
|
+
SidebarMenuSkeleton,
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// Export hooks
|
|
1008
|
+
export { useSidebar };
|
|
1009
|
+
|
|
1010
|
+
// Export context hook for backwards compatibility (deprecated)
|
|
1011
|
+
export { useSidebarContext };
|