@agent-native/toolkit 0.19.7 → 0.19.8-nightly-20260910195009
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/agent-native.eject.json +8 -1
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/index.d.ts.map +1 -1
- package/dist/app-shell/index.js +1 -0
- package/dist/app-shell/index.js.map +1 -1
- package/dist/app-shell/sidebar.d.ts +134 -0
- package/dist/app-shell/sidebar.d.ts.map +1 -0
- package/dist/app-shell/sidebar.js +183 -0
- package/dist/app-shell/sidebar.js.map +1 -0
- package/dist/app-shell/sidebar.spec.d.ts +2 -0
- package/dist/app-shell/sidebar.spec.d.ts.map +1 -0
- package/dist/app-shell/sidebar.spec.js +82 -0
- package/dist/app-shell/sidebar.spec.js.map +1 -0
- package/package.json +1 -1
- package/src/app-shell/index.ts +1 -0
- package/src/app-shell/sidebar.spec.tsx +142 -0
- package/src/app-shell/sidebar.tsx +1000 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconChevronDown,
|
|
3
|
+
IconLayoutSidebarLeftCollapse,
|
|
4
|
+
IconLayoutSidebarLeftExpand,
|
|
5
|
+
IconMessageCircle,
|
|
6
|
+
} from "@tabler/icons-react";
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
forwardRef,
|
|
10
|
+
isValidElement,
|
|
11
|
+
useCallback,
|
|
12
|
+
useContext,
|
|
13
|
+
useMemo,
|
|
14
|
+
useState,
|
|
15
|
+
type ComponentType,
|
|
16
|
+
type HTMLAttributes,
|
|
17
|
+
type MouseEvent,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from "react";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
Collapsible,
|
|
23
|
+
CollapsibleContent,
|
|
24
|
+
CollapsibleTrigger,
|
|
25
|
+
} from "../ui/collapsible.js";
|
|
26
|
+
import {
|
|
27
|
+
Tooltip,
|
|
28
|
+
TooltipContent,
|
|
29
|
+
TooltipProvider,
|
|
30
|
+
TooltipTrigger,
|
|
31
|
+
} from "../ui/tooltip.js";
|
|
32
|
+
import { cn } from "../utils.js";
|
|
33
|
+
import { usePersistentSidebarCollapsed } from "./use-persistent-sidebar-collapsed.js";
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Context
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export type AppSidebarLinkComponent = ComponentType<{
|
|
40
|
+
to?: string;
|
|
41
|
+
href?: string;
|
|
42
|
+
className?: string;
|
|
43
|
+
onClick?: (event: MouseEvent) => void;
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
"aria-label"?: string;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export interface AppSidebarContextValue {
|
|
49
|
+
collapsed: boolean;
|
|
50
|
+
setCollapsed: (collapsed: boolean | ((current: boolean) => boolean)) => void;
|
|
51
|
+
toggleCollapsed: () => void;
|
|
52
|
+
isMobile: boolean;
|
|
53
|
+
/** Router-aware link. Defaults to a native `<a href>`. */
|
|
54
|
+
LinkComponent: AppSidebarLinkComponent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const AppSidebarContext = createContext<AppSidebarContextValue | null>(null);
|
|
58
|
+
|
|
59
|
+
function NativeSidebarLink({
|
|
60
|
+
to,
|
|
61
|
+
href,
|
|
62
|
+
className,
|
|
63
|
+
onClick,
|
|
64
|
+
children,
|
|
65
|
+
"aria-label": ariaLabel,
|
|
66
|
+
}: {
|
|
67
|
+
to?: string;
|
|
68
|
+
href?: string;
|
|
69
|
+
className?: string;
|
|
70
|
+
onClick?: (event: MouseEvent) => void;
|
|
71
|
+
children?: ReactNode;
|
|
72
|
+
"aria-label"?: string;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<a
|
|
76
|
+
href={to ?? href}
|
|
77
|
+
className={className}
|
|
78
|
+
onClick={onClick}
|
|
79
|
+
aria-label={ariaLabel}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</a>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const defaultSidebarContextValue: AppSidebarContextValue = {
|
|
87
|
+
collapsed: false,
|
|
88
|
+
setCollapsed: () => {},
|
|
89
|
+
toggleCollapsed: () => {},
|
|
90
|
+
isMobile: false,
|
|
91
|
+
LinkComponent: NativeSidebarLink,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function useAppSidebar(): AppSidebarContextValue {
|
|
95
|
+
const context = useContext(AppSidebarContext);
|
|
96
|
+
return context ?? defaultSidebarContextValue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderSidebarIcon(
|
|
100
|
+
icon?: ComponentType<{ className?: string }> | ReactNode,
|
|
101
|
+
className = "size-4 shrink-0 text-primary",
|
|
102
|
+
): ReactNode {
|
|
103
|
+
if (!icon) return null;
|
|
104
|
+
if (isValidElement(icon)) {
|
|
105
|
+
return icon;
|
|
106
|
+
}
|
|
107
|
+
const IconComponent = icon as ComponentType<{ className?: string }>;
|
|
108
|
+
return <IconComponent className={className} />;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Item Definition Types
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface AppSidebarItemDefinition {
|
|
116
|
+
id?: string;
|
|
117
|
+
to?: string;
|
|
118
|
+
href?: string;
|
|
119
|
+
label: string;
|
|
120
|
+
icon?: ComponentType<{ className?: string }> | ReactNode;
|
|
121
|
+
active?: boolean;
|
|
122
|
+
count?: number | string;
|
|
123
|
+
badge?: ReactNode;
|
|
124
|
+
actions?: ReactNode;
|
|
125
|
+
onClick?: (event: MouseEvent) => void;
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Header
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
export interface AppSidebarHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
134
|
+
brandName?: ReactNode;
|
|
135
|
+
brandHref?: string;
|
|
136
|
+
brandIcon?: ReactNode;
|
|
137
|
+
brandLink?: ReactNode;
|
|
138
|
+
badge?: ReactNode;
|
|
139
|
+
onBrandClick?: (event: MouseEvent) => void;
|
|
140
|
+
collapsed?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const AppSidebarHeader = forwardRef<
|
|
144
|
+
HTMLDivElement,
|
|
145
|
+
AppSidebarHeaderProps
|
|
146
|
+
>(
|
|
147
|
+
(
|
|
148
|
+
{
|
|
149
|
+
brandName,
|
|
150
|
+
brandHref = "/",
|
|
151
|
+
brandIcon,
|
|
152
|
+
brandLink,
|
|
153
|
+
badge,
|
|
154
|
+
onBrandClick,
|
|
155
|
+
collapsed: propCollapsed,
|
|
156
|
+
className,
|
|
157
|
+
children,
|
|
158
|
+
...props
|
|
159
|
+
},
|
|
160
|
+
ref,
|
|
161
|
+
) => {
|
|
162
|
+
const context = useAppSidebar();
|
|
163
|
+
const collapsed = propCollapsed ?? context.collapsed;
|
|
164
|
+
const LinkComponent = context.LinkComponent;
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
ref={ref}
|
|
169
|
+
data-sidebar-header
|
|
170
|
+
className={cn(
|
|
171
|
+
"flex h-14 shrink-0 items-center border-b border-border",
|
|
172
|
+
collapsed ? "flex-col justify-center gap-0.5 px-2" : "gap-2 px-4",
|
|
173
|
+
className,
|
|
174
|
+
)}
|
|
175
|
+
{...props}
|
|
176
|
+
>
|
|
177
|
+
{brandLink ?? (
|
|
178
|
+
<LinkComponent
|
|
179
|
+
to={brandHref}
|
|
180
|
+
href={brandHref}
|
|
181
|
+
aria-label={typeof brandName === "string" ? brandName : undefined}
|
|
182
|
+
onClick={onBrandClick}
|
|
183
|
+
className={cn(
|
|
184
|
+
"flex min-w-0 items-center gap-2 rounded text-start outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
185
|
+
collapsed ? "size-8 justify-center" : "shrink-0",
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{brandIcon}
|
|
189
|
+
{!collapsed && brandName && (
|
|
190
|
+
<span className="truncate text-sm font-semibold text-primary">
|
|
191
|
+
{brandName}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</LinkComponent>
|
|
195
|
+
)}
|
|
196
|
+
{badge}
|
|
197
|
+
{children}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
AppSidebarHeader.displayName = "AppSidebarHeader";
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Nav Item
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
export interface AppSidebarNavItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
209
|
+
label: ReactNode;
|
|
210
|
+
icon?: ComponentType<{ className?: string }> | ReactNode;
|
|
211
|
+
to?: string;
|
|
212
|
+
href?: string;
|
|
213
|
+
active?: boolean;
|
|
214
|
+
count?: number | string;
|
|
215
|
+
badge?: ReactNode;
|
|
216
|
+
actions?: ReactNode;
|
|
217
|
+
onClick?: (event: MouseEvent) => void;
|
|
218
|
+
asChild?: boolean;
|
|
219
|
+
tooltipSide?: "right" | "top" | "bottom" | "left";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const AppSidebarNavItem = forwardRef<
|
|
223
|
+
HTMLDivElement,
|
|
224
|
+
AppSidebarNavItemProps
|
|
225
|
+
>(
|
|
226
|
+
(
|
|
227
|
+
{
|
|
228
|
+
label,
|
|
229
|
+
icon,
|
|
230
|
+
to,
|
|
231
|
+
href,
|
|
232
|
+
active = false,
|
|
233
|
+
count,
|
|
234
|
+
badge,
|
|
235
|
+
actions,
|
|
236
|
+
onClick,
|
|
237
|
+
asChild = false,
|
|
238
|
+
tooltipSide = "right",
|
|
239
|
+
className,
|
|
240
|
+
children,
|
|
241
|
+
...props
|
|
242
|
+
},
|
|
243
|
+
ref,
|
|
244
|
+
) => {
|
|
245
|
+
const { collapsed, LinkComponent } = useAppSidebar();
|
|
246
|
+
const linkHref = to ?? href;
|
|
247
|
+
|
|
248
|
+
if (collapsed) {
|
|
249
|
+
const tooltipLabel = typeof label === "string" ? label : undefined;
|
|
250
|
+
const content = asChild ? (
|
|
251
|
+
children
|
|
252
|
+
) : linkHref ? (
|
|
253
|
+
<LinkComponent
|
|
254
|
+
to={to}
|
|
255
|
+
href={href ?? to}
|
|
256
|
+
aria-label={tooltipLabel}
|
|
257
|
+
onClick={onClick}
|
|
258
|
+
className={cn(
|
|
259
|
+
"flex size-9 items-center justify-center rounded-md text-primary hover:bg-accent/60 hover:text-primary",
|
|
260
|
+
active &&
|
|
261
|
+
"bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary",
|
|
262
|
+
className,
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
{renderSidebarIcon(icon)}
|
|
266
|
+
</LinkComponent>
|
|
267
|
+
) : (
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
aria-label={tooltipLabel}
|
|
271
|
+
onClick={onClick}
|
|
272
|
+
className={cn(
|
|
273
|
+
"flex size-9 items-center justify-center rounded-md text-primary hover:bg-accent/60 hover:text-primary",
|
|
274
|
+
active &&
|
|
275
|
+
"bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary",
|
|
276
|
+
className,
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
279
|
+
{renderSidebarIcon(icon)}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<Tooltip>
|
|
285
|
+
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
286
|
+
<TooltipContent side={tooltipSide}>{label}</TooltipContent>
|
|
287
|
+
</Tooltip>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (asChild) {
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
ref={ref}
|
|
295
|
+
className={cn(
|
|
296
|
+
"group flex items-center rounded",
|
|
297
|
+
active
|
|
298
|
+
? "bg-primary/10 font-medium text-primary"
|
|
299
|
+
: "text-primary hover:bg-accent/60",
|
|
300
|
+
className,
|
|
301
|
+
)}
|
|
302
|
+
{...props}
|
|
303
|
+
>
|
|
304
|
+
{children}
|
|
305
|
+
{actions}
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const innerContent = (
|
|
311
|
+
<>
|
|
312
|
+
{renderSidebarIcon(icon)}
|
|
313
|
+
<span className="flex-1 truncate text-primary">{label}</span>
|
|
314
|
+
{count !== undefined &&
|
|
315
|
+
(typeof count === "number" ? count > 0 : Boolean(count)) && (
|
|
316
|
+
<span className="shrink-0 tabular-nums text-[11px] text-primary/80">
|
|
317
|
+
{count}
|
|
318
|
+
</span>
|
|
319
|
+
)}
|
|
320
|
+
{badge}
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
ref={ref}
|
|
327
|
+
className={cn(
|
|
328
|
+
"group flex items-center rounded",
|
|
329
|
+
active
|
|
330
|
+
? "bg-primary/10 font-medium text-primary"
|
|
331
|
+
: "text-primary hover:bg-accent/60",
|
|
332
|
+
className,
|
|
333
|
+
)}
|
|
334
|
+
{...props}
|
|
335
|
+
>
|
|
336
|
+
{linkHref ? (
|
|
337
|
+
<LinkComponent
|
|
338
|
+
to={to}
|
|
339
|
+
href={href ?? to}
|
|
340
|
+
onClick={onClick}
|
|
341
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-xs text-primary"
|
|
342
|
+
>
|
|
343
|
+
{innerContent}
|
|
344
|
+
</LinkComponent>
|
|
345
|
+
) : (
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
onClick={onClick}
|
|
349
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-xs text-primary"
|
|
350
|
+
>
|
|
351
|
+
{innerContent}
|
|
352
|
+
</button>
|
|
353
|
+
)}
|
|
354
|
+
{actions}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
AppSidebarNavItem.displayName = "AppSidebarNavItem";
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Nav Group (Collapsible)
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
export interface AppSidebarNavGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
366
|
+
label: ReactNode;
|
|
367
|
+
icon?: ComponentType<{ className?: string }> | ReactNode;
|
|
368
|
+
to?: string;
|
|
369
|
+
href?: string;
|
|
370
|
+
active?: boolean;
|
|
371
|
+
count?: number | string;
|
|
372
|
+
open?: boolean;
|
|
373
|
+
onOpenChange?: (open: boolean) => void;
|
|
374
|
+
defaultOpen?: boolean;
|
|
375
|
+
actions?: ReactNode;
|
|
376
|
+
children: ReactNode;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export const AppSidebarNavGroup = forwardRef<
|
|
380
|
+
HTMLDivElement,
|
|
381
|
+
AppSidebarNavGroupProps
|
|
382
|
+
>(
|
|
383
|
+
(
|
|
384
|
+
{
|
|
385
|
+
label,
|
|
386
|
+
icon,
|
|
387
|
+
to,
|
|
388
|
+
href,
|
|
389
|
+
active = false,
|
|
390
|
+
count,
|
|
391
|
+
open: controlledOpen,
|
|
392
|
+
onOpenChange: controlledOnOpenChange,
|
|
393
|
+
defaultOpen = false,
|
|
394
|
+
actions,
|
|
395
|
+
children,
|
|
396
|
+
className,
|
|
397
|
+
...props
|
|
398
|
+
},
|
|
399
|
+
ref,
|
|
400
|
+
) => {
|
|
401
|
+
const { collapsed, LinkComponent } = useAppSidebar();
|
|
402
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
403
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
404
|
+
const onOpenChange = controlledOnOpenChange ?? setUncontrolledOpen;
|
|
405
|
+
const linkHref = to ?? href;
|
|
406
|
+
|
|
407
|
+
if (collapsed) {
|
|
408
|
+
const tooltipLabel = typeof label === "string" ? label : undefined;
|
|
409
|
+
return (
|
|
410
|
+
<Tooltip>
|
|
411
|
+
<TooltipTrigger asChild>
|
|
412
|
+
<LinkComponent
|
|
413
|
+
to={to}
|
|
414
|
+
href={href ?? to ?? "#"}
|
|
415
|
+
aria-label={tooltipLabel}
|
|
416
|
+
className={cn(
|
|
417
|
+
"flex size-9 items-center justify-center rounded-md text-primary hover:bg-accent/60 hover:text-primary",
|
|
418
|
+
active &&
|
|
419
|
+
"bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary",
|
|
420
|
+
className,
|
|
421
|
+
)}
|
|
422
|
+
>
|
|
423
|
+
{renderSidebarIcon(icon)}
|
|
424
|
+
</LinkComponent>
|
|
425
|
+
</TooltipTrigger>
|
|
426
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
427
|
+
</Tooltip>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<Collapsible
|
|
433
|
+
open={open}
|
|
434
|
+
onOpenChange={onOpenChange}
|
|
435
|
+
className={cn("group/sidebar-nav", className)}
|
|
436
|
+
{...props}
|
|
437
|
+
>
|
|
438
|
+
<div
|
|
439
|
+
ref={ref}
|
|
440
|
+
className={cn(
|
|
441
|
+
"group flex items-center rounded",
|
|
442
|
+
active
|
|
443
|
+
? "bg-primary/10 font-medium text-primary"
|
|
444
|
+
: "text-primary hover:bg-accent/60",
|
|
445
|
+
)}
|
|
446
|
+
>
|
|
447
|
+
{linkHref ? (
|
|
448
|
+
<LinkComponent
|
|
449
|
+
to={to}
|
|
450
|
+
href={href ?? to}
|
|
451
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-xs text-primary"
|
|
452
|
+
>
|
|
453
|
+
{renderSidebarIcon(icon)}
|
|
454
|
+
<span className="flex-1 truncate text-primary">{label}</span>
|
|
455
|
+
{count !== undefined &&
|
|
456
|
+
(typeof count === "number" ? count > 0 : Boolean(count)) && (
|
|
457
|
+
<span className="shrink-0 tabular-nums text-[11px] text-primary/80">
|
|
458
|
+
{count}
|
|
459
|
+
</span>
|
|
460
|
+
)}
|
|
461
|
+
</LinkComponent>
|
|
462
|
+
) : (
|
|
463
|
+
<div className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-xs text-primary">
|
|
464
|
+
{renderSidebarIcon(icon)}
|
|
465
|
+
<span className="flex-1 truncate text-primary">{label}</span>
|
|
466
|
+
{count !== undefined &&
|
|
467
|
+
(typeof count === "number" ? count > 0 : Boolean(count)) && (
|
|
468
|
+
<span className="shrink-0 tabular-nums text-[11px] text-primary/80">
|
|
469
|
+
{count}
|
|
470
|
+
</span>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
{actions}
|
|
475
|
+
<CollapsibleTrigger asChild>
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
aria-label={open ? "Collapse" : "Expand"}
|
|
479
|
+
className="me-1 flex size-7 shrink-0 items-center justify-center rounded text-primary hover:bg-accent hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
480
|
+
>
|
|
481
|
+
<IconChevronDown
|
|
482
|
+
className={cn(
|
|
483
|
+
"size-3.5 transition-transform motion-reduce:transition-none",
|
|
484
|
+
open && "rotate-180",
|
|
485
|
+
)}
|
|
486
|
+
/>
|
|
487
|
+
</button>
|
|
488
|
+
</CollapsibleTrigger>
|
|
489
|
+
</div>
|
|
490
|
+
<CollapsibleContent className="clips-collapsible-content">
|
|
491
|
+
<div className="ms-3.5 border-s border-border/70 ps-2">
|
|
492
|
+
{children}
|
|
493
|
+
</div>
|
|
494
|
+
</CollapsibleContent>
|
|
495
|
+
</Collapsible>
|
|
496
|
+
);
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
AppSidebarNavGroup.displayName = "AppSidebarNavGroup";
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Section / Divider
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
export interface AppSidebarSectionProps extends Omit<
|
|
506
|
+
HTMLAttributes<HTMLDivElement>,
|
|
507
|
+
"title"
|
|
508
|
+
> {
|
|
509
|
+
title?: ReactNode;
|
|
510
|
+
divider?: boolean;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const AppSidebarSection = forwardRef<
|
|
514
|
+
HTMLDivElement,
|
|
515
|
+
AppSidebarSectionProps
|
|
516
|
+
>(({ title, divider = true, className, children, ...props }, ref) => {
|
|
517
|
+
const { collapsed } = useAppSidebar();
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
<div
|
|
521
|
+
ref={ref}
|
|
522
|
+
data-sidebar-section
|
|
523
|
+
className={cn(
|
|
524
|
+
divider && "border-t border-border/70",
|
|
525
|
+
collapsed
|
|
526
|
+
? "mt-2 flex flex-col items-center gap-1 pt-2"
|
|
527
|
+
: "mt-3 space-y-0.5 pt-3",
|
|
528
|
+
className,
|
|
529
|
+
)}
|
|
530
|
+
{...props}
|
|
531
|
+
>
|
|
532
|
+
{!collapsed && title && (
|
|
533
|
+
<p className="px-2 pb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/60">
|
|
534
|
+
{title}
|
|
535
|
+
</p>
|
|
536
|
+
)}
|
|
537
|
+
{children}
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
AppSidebarSection.displayName = "AppSidebarSection";
|
|
542
|
+
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// Feedback Button
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
export interface AppSidebarFeedbackButtonProps {
|
|
548
|
+
label?: string;
|
|
549
|
+
icon?: ComponentType<{ className?: string }> | ReactNode;
|
|
550
|
+
onClick?: (event: MouseEvent) => void;
|
|
551
|
+
className?: string;
|
|
552
|
+
collapsed?: boolean;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function AppSidebarFeedbackButton({
|
|
556
|
+
label = "Report an issue",
|
|
557
|
+
icon,
|
|
558
|
+
onClick,
|
|
559
|
+
className,
|
|
560
|
+
collapsed: propCollapsed,
|
|
561
|
+
}: AppSidebarFeedbackButtonProps) {
|
|
562
|
+
let contextCollapsed = false;
|
|
563
|
+
try {
|
|
564
|
+
const context = useAppSidebar();
|
|
565
|
+
contextCollapsed = context.collapsed;
|
|
566
|
+
} catch {
|
|
567
|
+
// coercion-ok: context is optional when AppSidebarFeedbackButton is used standalone
|
|
568
|
+
}
|
|
569
|
+
const isCollapsed = propCollapsed ?? contextCollapsed;
|
|
570
|
+
|
|
571
|
+
const button = (
|
|
572
|
+
<button
|
|
573
|
+
type="button"
|
|
574
|
+
aria-label={isCollapsed ? label : undefined}
|
|
575
|
+
onClick={onClick}
|
|
576
|
+
className={cn(
|
|
577
|
+
isCollapsed
|
|
578
|
+
? "flex size-9 items-center justify-center rounded-md bg-transparent text-primary hover:bg-accent/60 hover:text-primary"
|
|
579
|
+
: "flex h-auto w-full items-center justify-start gap-2 rounded bg-transparent px-2 py-1.5 text-xs font-normal text-primary hover:bg-accent/60 hover:text-primary",
|
|
580
|
+
className,
|
|
581
|
+
)}
|
|
582
|
+
>
|
|
583
|
+
{renderSidebarIcon(icon, "size-4 shrink-0 text-primary") ?? (
|
|
584
|
+
<IconMessageCircle className="size-4 shrink-0 text-primary" />
|
|
585
|
+
)}
|
|
586
|
+
{!isCollapsed && <span>{label}</span>}
|
|
587
|
+
</button>
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (!isCollapsed) return button;
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<Tooltip>
|
|
594
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
595
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
596
|
+
</Tooltip>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// Footer
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
export interface AppSidebarFooterProps extends HTMLAttributes<HTMLDivElement> {
|
|
605
|
+
feedback?: ReactNode;
|
|
606
|
+
orgSwitcher?: ReactNode;
|
|
607
|
+
footerExtras?: ReactNode;
|
|
608
|
+
collapsible?: boolean;
|
|
609
|
+
expandLabel?: string;
|
|
610
|
+
collapseLabel?: string;
|
|
611
|
+
collapsed?: boolean;
|
|
612
|
+
onToggleCollapsed?: () => void;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const AppSidebarFooter = forwardRef<
|
|
616
|
+
HTMLDivElement,
|
|
617
|
+
AppSidebarFooterProps
|
|
618
|
+
>(
|
|
619
|
+
(
|
|
620
|
+
{
|
|
621
|
+
feedback,
|
|
622
|
+
orgSwitcher,
|
|
623
|
+
footerExtras,
|
|
624
|
+
collapsible = true,
|
|
625
|
+
expandLabel = "Expand sidebar",
|
|
626
|
+
collapseLabel = "Collapse sidebar",
|
|
627
|
+
collapsed: propCollapsed,
|
|
628
|
+
onToggleCollapsed,
|
|
629
|
+
className,
|
|
630
|
+
children,
|
|
631
|
+
...props
|
|
632
|
+
},
|
|
633
|
+
ref,
|
|
634
|
+
) => {
|
|
635
|
+
const context = useAppSidebar();
|
|
636
|
+
const collapsed = propCollapsed ?? context.collapsed;
|
|
637
|
+
const toggleCollapsed = onToggleCollapsed ?? context.toggleCollapsed;
|
|
638
|
+
|
|
639
|
+
if (children) {
|
|
640
|
+
return (
|
|
641
|
+
<div
|
|
642
|
+
ref={ref}
|
|
643
|
+
data-sidebar-footer
|
|
644
|
+
className={cn(
|
|
645
|
+
"shrink-0 border-t border-border p-2",
|
|
646
|
+
collapsed ? "space-y-1" : "space-y-1.5",
|
|
647
|
+
className,
|
|
648
|
+
)}
|
|
649
|
+
{...props}
|
|
650
|
+
>
|
|
651
|
+
{children}
|
|
652
|
+
</div>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const collapseButton = collapsible ? (
|
|
657
|
+
<Tooltip>
|
|
658
|
+
<TooltipTrigger asChild>
|
|
659
|
+
<button
|
|
660
|
+
type="button"
|
|
661
|
+
aria-label={collapsed ? expandLabel : collapseLabel}
|
|
662
|
+
onClick={toggleCollapsed}
|
|
663
|
+
className="flex size-9 shrink-0 items-center justify-center rounded-md bg-transparent text-primary hover:bg-accent/60 hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
664
|
+
>
|
|
665
|
+
{collapsed ? (
|
|
666
|
+
<IconLayoutSidebarLeftExpand className="size-4 rtl:-scale-x-100" />
|
|
667
|
+
) : (
|
|
668
|
+
<IconLayoutSidebarLeftCollapse className="size-4 rtl:-scale-x-100" />
|
|
669
|
+
)}
|
|
670
|
+
</button>
|
|
671
|
+
</TooltipTrigger>
|
|
672
|
+
<TooltipContent side="right">
|
|
673
|
+
{collapsed ? expandLabel : collapseLabel}
|
|
674
|
+
</TooltipContent>
|
|
675
|
+
</Tooltip>
|
|
676
|
+
) : null;
|
|
677
|
+
|
|
678
|
+
const resolvedFeedback =
|
|
679
|
+
feedback !== undefined ? (
|
|
680
|
+
feedback
|
|
681
|
+
) : (
|
|
682
|
+
<AppSidebarFeedbackButton collapsed={collapsed} />
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
return (
|
|
686
|
+
<div
|
|
687
|
+
ref={ref}
|
|
688
|
+
data-sidebar-footer
|
|
689
|
+
className={cn(
|
|
690
|
+
"shrink-0 border-t border-border p-2",
|
|
691
|
+
collapsed ? "space-y-1" : "space-y-1.5",
|
|
692
|
+
className,
|
|
693
|
+
)}
|
|
694
|
+
{...props}
|
|
695
|
+
>
|
|
696
|
+
{resolvedFeedback}
|
|
697
|
+
<div
|
|
698
|
+
data-sidebar-footer-utilities
|
|
699
|
+
className={cn(
|
|
700
|
+
collapsed
|
|
701
|
+
? "flex flex-col items-center gap-1"
|
|
702
|
+
: "flex items-center gap-0.5",
|
|
703
|
+
)}
|
|
704
|
+
>
|
|
705
|
+
{orgSwitcher}
|
|
706
|
+
{footerExtras}
|
|
707
|
+
{collapseButton}
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
);
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
AppSidebarFooter.displayName = "AppSidebarFooter";
|
|
714
|
+
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
// AppSidebar (Main Component)
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
export interface AppSidebarProps extends HTMLAttributes<HTMLElement> {
|
|
720
|
+
// Collapse state
|
|
721
|
+
collapsed?: boolean;
|
|
722
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
723
|
+
defaultCollapsed?: boolean;
|
|
724
|
+
collapsible?: boolean;
|
|
725
|
+
storageKey?: string;
|
|
726
|
+
isMobile?: boolean;
|
|
727
|
+
/**
|
|
728
|
+
* When set (boolean), the sidebar owns off-canvas mobile drawer positioning.
|
|
729
|
+
* Leave undefined when embedding inside an external Sheet/drawer so the
|
|
730
|
+
* sidebar stays visible without translate offsets.
|
|
731
|
+
*/
|
|
732
|
+
mobileOpen?: boolean;
|
|
733
|
+
onMobileOpenChange?: (open: boolean) => void;
|
|
734
|
+
/** Router-aware link component. Defaults to a native `<a href>`. */
|
|
735
|
+
linkComponent?: AppSidebarLinkComponent;
|
|
736
|
+
|
|
737
|
+
// Header
|
|
738
|
+
brandName?: ReactNode;
|
|
739
|
+
brandHref?: string;
|
|
740
|
+
brandIcon?: ReactNode;
|
|
741
|
+
brandLink?: ReactNode;
|
|
742
|
+
badge?: ReactNode;
|
|
743
|
+
headerContent?: ReactNode;
|
|
744
|
+
|
|
745
|
+
// Items
|
|
746
|
+
items?: AppSidebarItemDefinition[];
|
|
747
|
+
secondaryItems?: AppSidebarItemDefinition[];
|
|
748
|
+
|
|
749
|
+
// Footer
|
|
750
|
+
feedback?: ReactNode;
|
|
751
|
+
orgSwitcher?: ReactNode;
|
|
752
|
+
footerExtras?: ReactNode;
|
|
753
|
+
footerContent?: ReactNode;
|
|
754
|
+
expandLabel?: string;
|
|
755
|
+
collapseLabel?: string;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export const AppSidebar = forwardRef<HTMLElement, AppSidebarProps>(
|
|
759
|
+
(
|
|
760
|
+
{
|
|
761
|
+
collapsed: controlledCollapsed,
|
|
762
|
+
onCollapsedChange: controlledOnCollapsedChange,
|
|
763
|
+
defaultCollapsed = false,
|
|
764
|
+
collapsible = true,
|
|
765
|
+
storageKey,
|
|
766
|
+
isMobile = false,
|
|
767
|
+
mobileOpen,
|
|
768
|
+
onMobileOpenChange,
|
|
769
|
+
linkComponent,
|
|
770
|
+
|
|
771
|
+
brandName,
|
|
772
|
+
brandHref = "/",
|
|
773
|
+
brandIcon,
|
|
774
|
+
brandLink,
|
|
775
|
+
badge,
|
|
776
|
+
headerContent,
|
|
777
|
+
|
|
778
|
+
items,
|
|
779
|
+
secondaryItems,
|
|
780
|
+
|
|
781
|
+
feedback,
|
|
782
|
+
orgSwitcher,
|
|
783
|
+
footerExtras,
|
|
784
|
+
footerContent,
|
|
785
|
+
expandLabel = "Expand sidebar",
|
|
786
|
+
collapseLabel = "Collapse sidebar",
|
|
787
|
+
|
|
788
|
+
className,
|
|
789
|
+
children,
|
|
790
|
+
...props
|
|
791
|
+
},
|
|
792
|
+
ref,
|
|
793
|
+
) => {
|
|
794
|
+
// Persistence hook if storageKey is provided and not controlled
|
|
795
|
+
const persistent = usePersistentSidebarCollapsed({
|
|
796
|
+
storageKey: storageKey ?? "app-sidebar-default-key",
|
|
797
|
+
defaultCollapsed,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const isControlled = controlledCollapsed !== undefined;
|
|
801
|
+
const isUsingStorage = !isControlled && Boolean(storageKey);
|
|
802
|
+
|
|
803
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] =
|
|
804
|
+
useState(defaultCollapsed);
|
|
805
|
+
|
|
806
|
+
const collapsed = isControlled
|
|
807
|
+
? controlledCollapsed!
|
|
808
|
+
: isUsingStorage
|
|
809
|
+
? persistent.collapsed
|
|
810
|
+
: uncontrolledCollapsed;
|
|
811
|
+
|
|
812
|
+
const setCollapsed = useCallback(
|
|
813
|
+
(value: boolean | ((current: boolean) => boolean)) => {
|
|
814
|
+
const next = typeof value === "function" ? value(collapsed) : value;
|
|
815
|
+
if (controlledOnCollapsedChange) {
|
|
816
|
+
controlledOnCollapsedChange(next);
|
|
817
|
+
}
|
|
818
|
+
if (isUsingStorage) {
|
|
819
|
+
persistent.setCollapsed(next);
|
|
820
|
+
}
|
|
821
|
+
if (!isControlled && !isUsingStorage) {
|
|
822
|
+
setUncontrolledCollapsed(next);
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
[
|
|
826
|
+
collapsed,
|
|
827
|
+
controlledOnCollapsedChange,
|
|
828
|
+
isControlled,
|
|
829
|
+
isUsingStorage,
|
|
830
|
+
persistent,
|
|
831
|
+
],
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
const toggleCollapsed = useCallback(() => {
|
|
835
|
+
setCollapsed((prev) => !prev);
|
|
836
|
+
}, [setCollapsed]);
|
|
837
|
+
|
|
838
|
+
const showCollapsedSidebar = collapsed && !isMobile;
|
|
839
|
+
|
|
840
|
+
const LinkComponent = linkComponent ?? NativeSidebarLink;
|
|
841
|
+
// Only self-manage off-canvas transform when the caller controls mobileOpen.
|
|
842
|
+
// Sheet/drawer embeds leave mobileOpen undefined so content stays visible.
|
|
843
|
+
const ownsMobileDrawer = typeof mobileOpen === "boolean";
|
|
844
|
+
|
|
845
|
+
const contextValue = useMemo<AppSidebarContextValue>(
|
|
846
|
+
() => ({
|
|
847
|
+
collapsed: showCollapsedSidebar,
|
|
848
|
+
setCollapsed,
|
|
849
|
+
toggleCollapsed,
|
|
850
|
+
isMobile,
|
|
851
|
+
LinkComponent,
|
|
852
|
+
}),
|
|
853
|
+
[
|
|
854
|
+
showCollapsedSidebar,
|
|
855
|
+
setCollapsed,
|
|
856
|
+
toggleCollapsed,
|
|
857
|
+
isMobile,
|
|
858
|
+
LinkComponent,
|
|
859
|
+
],
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
return (
|
|
863
|
+
<AppSidebarContext.Provider value={contextValue}>
|
|
864
|
+
<TooltipProvider delayDuration={0}>
|
|
865
|
+
<aside
|
|
866
|
+
ref={ref}
|
|
867
|
+
data-collapsed={showCollapsedSidebar ? "true" : "false"}
|
|
868
|
+
className={cn(
|
|
869
|
+
"flex h-full w-[260px] flex-col overflow-hidden border-e border-border bg-sidebar transition-[width,transform] duration-200 ease-out",
|
|
870
|
+
ownsMobileDrawer
|
|
871
|
+
? "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 md:static md:z-auto"
|
|
872
|
+
: "relative md:static",
|
|
873
|
+
showCollapsedSidebar && "md:w-14",
|
|
874
|
+
ownsMobileDrawer &&
|
|
875
|
+
(mobileOpen
|
|
876
|
+
? "translate-x-0"
|
|
877
|
+
: "-translate-x-full rtl:translate-x-full md:translate-x-0"),
|
|
878
|
+
className,
|
|
879
|
+
)}
|
|
880
|
+
{...props}
|
|
881
|
+
>
|
|
882
|
+
{headerContent ?? (
|
|
883
|
+
<AppSidebarHeader
|
|
884
|
+
brandName={brandName}
|
|
885
|
+
brandHref={brandHref}
|
|
886
|
+
brandIcon={brandIcon}
|
|
887
|
+
brandLink={brandLink}
|
|
888
|
+
badge={badge}
|
|
889
|
+
/>
|
|
890
|
+
)}
|
|
891
|
+
|
|
892
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
893
|
+
{showCollapsedSidebar ? (
|
|
894
|
+
<nav className="flex flex-col items-center gap-1 px-2 py-3">
|
|
895
|
+
{items?.map((item) => (
|
|
896
|
+
<AppSidebarNavItem
|
|
897
|
+
key={item.id ?? item.to ?? item.href ?? item.label}
|
|
898
|
+
to={item.to}
|
|
899
|
+
href={item.href}
|
|
900
|
+
label={item.label}
|
|
901
|
+
icon={item.icon}
|
|
902
|
+
active={item.active}
|
|
903
|
+
count={item.count}
|
|
904
|
+
badge={item.badge}
|
|
905
|
+
onClick={item.onClick}
|
|
906
|
+
/>
|
|
907
|
+
))}
|
|
908
|
+
{children}
|
|
909
|
+
{secondaryItems && secondaryItems.length > 0 && (
|
|
910
|
+
<div className="mt-2 flex flex-col items-center gap-1 border-t border-border/70 pt-2">
|
|
911
|
+
{secondaryItems.map((item) => (
|
|
912
|
+
<AppSidebarNavItem
|
|
913
|
+
key={item.id ?? item.to ?? item.href ?? item.label}
|
|
914
|
+
to={item.to}
|
|
915
|
+
href={item.href}
|
|
916
|
+
label={item.label}
|
|
917
|
+
icon={item.icon}
|
|
918
|
+
active={item.active}
|
|
919
|
+
count={item.count}
|
|
920
|
+
badge={item.badge}
|
|
921
|
+
onClick={item.onClick}
|
|
922
|
+
/>
|
|
923
|
+
))}
|
|
924
|
+
</div>
|
|
925
|
+
)}
|
|
926
|
+
</nav>
|
|
927
|
+
) : (
|
|
928
|
+
<nav className="space-y-0.5 px-2 py-3">
|
|
929
|
+
{items?.map((item) => {
|
|
930
|
+
if (item.children) {
|
|
931
|
+
return (
|
|
932
|
+
<AppSidebarNavGroup
|
|
933
|
+
key={item.id ?? item.to ?? item.href ?? item.label}
|
|
934
|
+
to={item.to}
|
|
935
|
+
href={item.href}
|
|
936
|
+
label={item.label}
|
|
937
|
+
icon={item.icon}
|
|
938
|
+
active={item.active}
|
|
939
|
+
count={item.count}
|
|
940
|
+
actions={item.actions}
|
|
941
|
+
>
|
|
942
|
+
{item.children}
|
|
943
|
+
</AppSidebarNavGroup>
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return (
|
|
947
|
+
<AppSidebarNavItem
|
|
948
|
+
key={item.id ?? item.to ?? item.href ?? item.label}
|
|
949
|
+
to={item.to}
|
|
950
|
+
href={item.href}
|
|
951
|
+
label={item.label}
|
|
952
|
+
icon={item.icon}
|
|
953
|
+
active={item.active}
|
|
954
|
+
count={item.count}
|
|
955
|
+
badge={item.badge}
|
|
956
|
+
actions={item.actions}
|
|
957
|
+
onClick={item.onClick}
|
|
958
|
+
/>
|
|
959
|
+
);
|
|
960
|
+
})}
|
|
961
|
+
{children}
|
|
962
|
+
{secondaryItems && secondaryItems.length > 0 && (
|
|
963
|
+
<div className="mt-3 space-y-0.5 border-t border-border/70 pt-3">
|
|
964
|
+
{secondaryItems.map((item) => (
|
|
965
|
+
<AppSidebarNavItem
|
|
966
|
+
key={item.id ?? item.to ?? item.href ?? item.label}
|
|
967
|
+
to={item.to}
|
|
968
|
+
href={item.href}
|
|
969
|
+
label={item.label}
|
|
970
|
+
icon={item.icon}
|
|
971
|
+
active={item.active}
|
|
972
|
+
count={item.count}
|
|
973
|
+
badge={item.badge}
|
|
974
|
+
actions={item.actions}
|
|
975
|
+
onClick={item.onClick}
|
|
976
|
+
/>
|
|
977
|
+
))}
|
|
978
|
+
</div>
|
|
979
|
+
)}
|
|
980
|
+
</nav>
|
|
981
|
+
)}
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
{footerContent ?? (
|
|
985
|
+
<AppSidebarFooter
|
|
986
|
+
feedback={feedback}
|
|
987
|
+
orgSwitcher={orgSwitcher}
|
|
988
|
+
footerExtras={footerExtras}
|
|
989
|
+
collapsible={collapsible}
|
|
990
|
+
expandLabel={expandLabel}
|
|
991
|
+
collapseLabel={collapseLabel}
|
|
992
|
+
/>
|
|
993
|
+
)}
|
|
994
|
+
</aside>
|
|
995
|
+
</TooltipProvider>
|
|
996
|
+
</AppSidebarContext.Provider>
|
|
997
|
+
);
|
|
998
|
+
},
|
|
999
|
+
);
|
|
1000
|
+
AppSidebar.displayName = "AppSidebar";
|