@olympusoss/canvas 2.20.2 → 3.2.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 (214) hide show
  1. package/README.md +73 -35
  2. package/package.json +46 -177
  3. package/src/cn.ts +3 -0
  4. package/src/index.ts +12 -603
  5. package/src/theme.ts +62 -0
  6. package/src/tokens.ts +11 -0
  7. package/styles/atoms/avatar.css +22 -0
  8. package/styles/atoms/badge.css +83 -0
  9. package/styles/atoms/breadcrumb.css +35 -0
  10. package/styles/atoms/button-group.css +23 -0
  11. package/styles/atoms/button.css +107 -0
  12. package/styles/atoms/checkbox.css +55 -0
  13. package/styles/atoms/combobox.css +75 -0
  14. package/styles/atoms/dropdown.css +54 -0
  15. package/styles/atoms/icon.css +8 -0
  16. package/styles/atoms/input-group.css +45 -0
  17. package/styles/atoms/input.css +56 -0
  18. package/styles/atoms/kbd.css +15 -0
  19. package/styles/atoms/pagination.css +48 -0
  20. package/styles/atoms/popover.css +14 -0
  21. package/styles/atoms/radio.css +28 -0
  22. package/styles/atoms/select.css +57 -0
  23. package/styles/atoms/separator.css +32 -0
  24. package/styles/atoms/skeleton.css +32 -0
  25. package/styles/atoms/spinner.css +26 -0
  26. package/styles/atoms/switch.css +45 -0
  27. package/styles/atoms/textarea.css +31 -0
  28. package/styles/atoms/tooltip.css +53 -0
  29. package/styles/atoms/typography.css +105 -0
  30. package/styles/base.css +17 -0
  31. package/styles/canvas.css +77 -52
  32. package/styles/molecules/alert.css +66 -0
  33. package/styles/molecules/card.css +58 -0
  34. package/styles/molecules/code-block.css +18 -0
  35. package/styles/molecules/empty-state.css +17 -0
  36. package/styles/molecules/field.css +27 -0
  37. package/styles/molecules/form.css +27 -0
  38. package/styles/molecules/page-header.css +52 -0
  39. package/styles/molecules/section-card.css +49 -0
  40. package/styles/molecules/stat-card.css +71 -0
  41. package/styles/molecules/toast.css +95 -0
  42. package/styles/organisms/app-shell.css +46 -0
  43. package/styles/organisms/calendar.css +73 -0
  44. package/styles/organisms/command.css +94 -0
  45. package/styles/organisms/data-table.css +142 -0
  46. package/styles/organisms/dialog.css +72 -0
  47. package/styles/organisms/filter-panel.css +58 -0
  48. package/styles/organisms/row-menu.css +69 -0
  49. package/styles/organisms/sheet.css +70 -0
  50. package/styles/organisms/sidebar.css +146 -0
  51. package/styles/organisms/stepper.css +63 -0
  52. package/styles/organisms/tabs.css +40 -0
  53. package/styles/organisms/topbar.css +24 -0
  54. package/styles/patterns/backdrops.css +35 -0
  55. package/styles/patterns/density.css +66 -0
  56. package/styles/patterns/focus.css +22 -0
  57. package/styles/patterns/glass.css +85 -0
  58. package/styles/patterns/high-contrast.css +70 -0
  59. package/styles/patterns/reduced-motion.css +12 -0
  60. package/styles/patterns/scrollbar.css +10 -0
  61. package/styles/reset.css +89 -0
  62. package/styles/tokens/colors.css +106 -0
  63. package/styles/tokens/motion.css +33 -0
  64. package/styles/tokens/radius.css +10 -0
  65. package/styles/tokens/shadows.css +35 -0
  66. package/styles/tokens/spacing.css +19 -0
  67. package/styles/tokens/typography.css +6 -0
  68. package/styles/tokens/z-index.css +12 -0
  69. package/styles/utilities/display.css +66 -0
  70. package/styles/utilities/flexbox.css +240 -0
  71. package/styles/utilities/gap.css +288 -0
  72. package/styles/utilities/grid.css +138 -0
  73. package/styles/utilities/position.css +78 -0
  74. package/styles/utilities/sizing.css +138 -0
  75. package/tsconfig.json +20 -21
  76. package/src/components/atoms/README.md +0 -11
  77. package/src/components/atoms/aspect-ratio.tsx +0 -32
  78. package/src/components/atoms/avatar.tsx +0 -98
  79. package/src/components/atoms/badge.tsx +0 -44
  80. package/src/components/atoms/brand-mark.tsx +0 -74
  81. package/src/components/atoms/button.tsx +0 -105
  82. package/src/components/atoms/checkbox.tsx +0 -63
  83. package/src/components/atoms/flex-box.tsx +0 -105
  84. package/src/components/atoms/icon.tsx +0 -34
  85. package/src/components/atoms/input.tsx +0 -92
  86. package/src/components/atoms/label.tsx +0 -41
  87. package/src/components/atoms/logo.tsx +0 -89
  88. package/src/components/atoms/progress.tsx +0 -55
  89. package/src/components/atoms/radio-group.tsx +0 -122
  90. package/src/components/atoms/scroll-area.tsx +0 -106
  91. package/src/components/atoms/section.tsx +0 -48
  92. package/src/components/atoms/separator.tsx +0 -45
  93. package/src/components/atoms/skeleton.tsx +0 -17
  94. package/src/components/atoms/slider.tsx +0 -93
  95. package/src/components/atoms/spinner.tsx +0 -47
  96. package/src/components/atoms/switch.tsx +0 -60
  97. package/src/components/atoms/textarea.tsx +0 -78
  98. package/src/components/atoms/toggle.tsx +0 -80
  99. package/src/components/charts/activity-heatmap.tsx +0 -186
  100. package/src/components/charts/axes.tsx +0 -21
  101. package/src/components/charts/chart-container.tsx +0 -254
  102. package/src/components/charts/chart-legend.tsx +0 -67
  103. package/src/components/charts/chart-tooltip.tsx +0 -161
  104. package/src/components/charts/chart-types.tsx +0 -49
  105. package/src/components/charts/containers.tsx +0 -11
  106. package/src/components/charts/data.tsx +0 -16
  107. package/src/components/charts/details.tsx +0 -25
  108. package/src/components/charts/dot-pulse.tsx +0 -61
  109. package/src/components/charts/gauge.tsx +0 -106
  110. package/src/components/charts/grids.tsx +0 -8
  111. package/src/components/charts/index.ts +0 -62
  112. package/src/components/charts/labeled-bar-list.tsx +0 -85
  113. package/src/components/charts/metric-breakdown.tsx +0 -316
  114. package/src/components/charts/references.tsx +0 -8
  115. package/src/components/charts/service-health-list.tsx +0 -85
  116. package/src/components/charts/sparkline-area.tsx +0 -80
  117. package/src/components/charts/sparkline.tsx +0 -52
  118. package/src/components/charts/stacked-bar.tsx +0 -104
  119. package/src/components/charts/text.tsx +0 -10
  120. package/src/components/charts/world-heat-map-inner.tsx +0 -317
  121. package/src/components/charts/world-heat-map.tsx +0 -184
  122. package/src/components/molecules/README.md +0 -12
  123. package/src/components/molecules/action-bar.tsx +0 -73
  124. package/src/components/molecules/activity-item.tsx +0 -74
  125. package/src/components/molecules/alert.tsx +0 -86
  126. package/src/components/molecules/animated-background.tsx +0 -92
  127. package/src/components/molecules/auth-shell.tsx +0 -95
  128. package/src/components/molecules/brand-lockup.tsx +0 -48
  129. package/src/components/molecules/breadcrumb.tsx +0 -157
  130. package/src/components/molecules/button-group.tsx +0 -104
  131. package/src/components/molecules/calendar.tsx +0 -217
  132. package/src/components/molecules/card.tsx +0 -102
  133. package/src/components/molecules/client-brand.tsx +0 -95
  134. package/src/components/molecules/code-block.tsx +0 -86
  135. package/src/components/molecules/countdown-button.tsx +0 -92
  136. package/src/components/molecules/empty-state.tsx +0 -56
  137. package/src/components/molecules/error-state.tsx +0 -42
  138. package/src/components/molecules/field-display.tsx +0 -35
  139. package/src/components/molecules/input-otp.tsx +0 -74
  140. package/src/components/molecules/launcher-card.tsx +0 -152
  141. package/src/components/molecules/loading-state.tsx +0 -36
  142. package/src/components/molecules/notification-item.tsx +0 -67
  143. package/src/components/molecules/notification-list.tsx +0 -45
  144. package/src/components/molecules/number-badge.tsx +0 -53
  145. package/src/components/molecules/or-separator.tsx +0 -38
  146. package/src/components/molecules/page-header.tsx +0 -88
  147. package/src/components/molecules/page-tabs.tsx +0 -94
  148. package/src/components/molecules/pagination.tsx +0 -150
  149. package/src/components/molecules/password-input.tsx +0 -83
  150. package/src/components/molecules/password-strength-meter.tsx +0 -104
  151. package/src/components/molecules/phone-input.tsx +0 -200
  152. package/src/components/molecules/search-bar.tsx +0 -64
  153. package/src/components/molecules/secret-field.tsx +0 -158
  154. package/src/components/molecules/section-card.tsx +0 -91
  155. package/src/components/molecules/social-buttons.tsx +0 -165
  156. package/src/components/molecules/stat-card.tsx +0 -100
  157. package/src/components/molecules/status-badge.tsx +0 -42
  158. package/src/components/molecules/stepper.tsx +0 -96
  159. package/src/components/molecules/table.tsx +0 -157
  160. package/src/components/molecules/terminal.tsx +0 -74
  161. package/src/components/molecules/toggle-group.tsx +0 -145
  162. package/src/components/molecules/tooltip.tsx +0 -155
  163. package/src/components/molecules/user-avatar-chip.tsx +0 -71
  164. package/src/components/organisms/README.md +0 -14
  165. package/src/components/organisms/accordion.tsx +0 -154
  166. package/src/components/organisms/alert-dialog.tsx +0 -277
  167. package/src/components/organisms/carousel.tsx +0 -244
  168. package/src/components/organisms/collapsible.tsx +0 -69
  169. package/src/components/organisms/command.tsx +0 -144
  170. package/src/components/organisms/context-menu.tsx +0 -339
  171. package/src/components/organisms/dashboard-grid.tsx +0 -369
  172. package/src/components/organisms/data-table.tsx +0 -330
  173. package/src/components/organisms/dialog.tsx +0 -312
  174. package/src/components/organisms/drawer.tsx +0 -123
  175. package/src/components/organisms/dropdown-menu.tsx +0 -440
  176. package/src/components/organisms/editors/code-editor.tsx +0 -144
  177. package/src/components/organisms/editors/index.ts +0 -4
  178. package/src/components/organisms/editors/markdown-editor.tsx +0 -153
  179. package/src/components/organisms/editors/markdown-renderer.ts +0 -27
  180. package/src/components/organisms/editors/prose-canvas-classes.ts +0 -45
  181. package/src/components/organisms/editors/rich-text-editor.tsx +0 -126
  182. package/src/components/organisms/editors/toolbar/md-toolbar.tsx +0 -129
  183. package/src/components/organisms/editors/toolbar/rte-toolbar.tsx +0 -211
  184. package/src/components/organisms/editors/toolbar/toolbar-shell.tsx +0 -45
  185. package/src/components/organisms/editors/use-codemirror-theme.ts +0 -61
  186. package/src/components/organisms/error-boundary.tsx +0 -61
  187. package/src/components/organisms/form.tsx +0 -174
  188. package/src/components/organisms/hover-card.tsx +0 -115
  189. package/src/components/organisms/menubar.tsx +0 -498
  190. package/src/components/organisms/navbar.tsx +0 -104
  191. package/src/components/organisms/navigation-menu.tsx +0 -235
  192. package/src/components/organisms/popover.tsx +0 -149
  193. package/src/components/organisms/resizable.tsx +0 -58
  194. package/src/components/organisms/schema-form.tsx +0 -232
  195. package/src/components/organisms/select.tsx +0 -309
  196. package/src/components/organisms/sheet.tsx +0 -265
  197. package/src/components/organisms/sidebar.tsx +0 -1040
  198. package/src/components/organisms/sonner.tsx +0 -96
  199. package/src/components/organisms/tabs.tsx +0 -133
  200. package/src/components/organisms/theme-provider.tsx +0 -101
  201. package/src/hooks/use-mobile.tsx +0 -19
  202. package/src/lib/portal-container.tsx +0 -35
  203. package/src/lib/utils.ts +0 -6
  204. package/src/native.ts +0 -23
  205. package/src/tokens/colors.ts +0 -91
  206. package/src/tokens/index.ts +0 -3
  207. package/src/tokens/spacing.ts +0 -55
  208. package/src/tokens/typography.ts +0 -27
  209. package/styles/dashboard-grid.css +0 -47
  210. package/styles/fonts/Roboto-VariableFont_wdth_wght.ttf +0 -0
  211. package/styles/glass.css +0 -175
  212. package/styles/leaflet.css +0 -13
  213. package/styles/tokens.css +0 -317
  214. package/tailwind.config.ts +0 -70
@@ -1,1040 +0,0 @@
1
- "use client";
2
-
3
- import { Slot } from "@radix-ui/react-slot";
4
- import { cva, type VariantProps } from "class-variance-authority";
5
- import { PanelLeft } from "lucide-react";
6
- import * as React from "react";
7
-
8
- import { useIsMobile } from "../../hooks/use-mobile";
9
- import { cn } from "../../lib/utils";
10
- import { Button } from "../atoms/button";
11
- import { Input } from "../atoms/input";
12
- import { ScrollArea } from "../atoms/scroll-area";
13
- import { Separator } from "../atoms/separator";
14
- import { Skeleton } from "../atoms/skeleton";
15
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../molecules/tooltip";
16
- import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./sheet";
17
-
18
- const SIDEBAR_COOKIE_NAME = "sidebar_state";
19
- const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
20
- const SIDEBAR_WIDTH = "15rem";
21
- const SIDEBAR_WIDTH_MOBILE = "18rem";
22
- const SIDEBAR_WIDTH_ICON = "3.5rem";
23
- const SIDEBAR_KEYBOARD_SHORTCUT = "b";
24
-
25
- type SidebarContextProps = {
26
- state: "expanded" | "collapsed";
27
- open: boolean;
28
- setOpen: (open: boolean) => void;
29
- openMobile: boolean;
30
- setOpenMobile: (open: boolean) => void;
31
- isMobile: boolean;
32
- toggleSidebar: () => void;
33
- };
34
-
35
- const SidebarContext = React.createContext<SidebarContextProps | null>(null);
36
-
37
- function useSidebar() {
38
- const context = React.useContext(SidebarContext);
39
- if (!context) {
40
- throw new Error("useSidebar must be used within a SidebarProvider.");
41
- }
42
-
43
- return context;
44
- }
45
-
46
- export interface SidebarProviderProps extends React.ComponentProps<"div"> {
47
- /**
48
- * Initial open state when uncontrolled.
49
- * @default true
50
- */
51
- defaultOpen?: boolean;
52
- /** Controlled open state. Pair with `onOpenChange`. */
53
- open?: boolean;
54
- /** Fires with the next open state. */
55
- onOpenChange?: (open: boolean) => void;
56
- /** Sidebar + Inset + page content. */
57
- children?: React.ReactNode;
58
- className?: string;
59
- style?: React.CSSProperties;
60
- }
61
-
62
- /**
63
- * Wraps the entire shell and supplies the `useSidebar()` context. Reads the
64
- * `--sidebar-width` / `--sidebar-width-icon` CSS variables off this element,
65
- * so any width override goes here via `style`. Required around every Sidebar.
66
- */
67
- const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
68
- (
69
- {
70
- defaultOpen = true,
71
- open: openProp,
72
- onOpenChange: setOpenProp,
73
- className,
74
- style,
75
- children,
76
- ...props
77
- },
78
- ref,
79
- ) => {
80
- const isMobile = useIsMobile();
81
- const [openMobile, setOpenMobile] = React.useState(false);
82
-
83
- // This is the internal state of the sidebar.
84
- // We use openProp and setOpenProp for control from outside the component.
85
- const [_open, _setOpen] = React.useState(defaultOpen);
86
- const open = openProp ?? _open;
87
- const setOpen = React.useCallback(
88
- (value: boolean | ((value: boolean) => boolean)) => {
89
- /* c8 ignore next -- direct-boolean branch: internal toggleSidebar only ever passes a function updater */
90
- const openState = typeof value === "function" ? value(open) : value;
91
- if (setOpenProp) {
92
- setOpenProp(openState);
93
- } else {
94
- _setOpen(openState);
95
- }
96
-
97
- // This sets the cookie to keep the sidebar state.
98
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
99
- },
100
- [setOpenProp, open],
101
- );
102
-
103
- // Helper to toggle the sidebar.
104
- const toggleSidebar = React.useCallback(() => {
105
- /* c8 ignore next -- branch coverage for the mobile vs desktop sides is split across mocked-and-unmocked test contexts */
106
- return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
107
- }, [isMobile, setOpen]);
108
-
109
- // Adds a keyboard shortcut to toggle the sidebar.
110
- React.useEffect(() => {
111
- const handleKeyDown = (event: KeyboardEvent) => {
112
- /* c8 ignore next 4 -- branch coverage for the metaKey path vs ctrlKey is combinatorial; the global handler's core path is tested */
113
- if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
114
- event.preventDefault();
115
- toggleSidebar();
116
- }
117
- };
118
-
119
- window.addEventListener("keydown", handleKeyDown);
120
- return () => window.removeEventListener("keydown", handleKeyDown);
121
- }, [toggleSidebar]);
122
-
123
- // We add a state so that we can do data-state="expanded" or "collapsed".
124
- // This makes it easier to style the sidebar with Tailwind classes.
125
- const state = open ? "expanded" : "collapsed";
126
-
127
- const contextValue = React.useMemo<SidebarContextProps>(
128
- () => ({
129
- state,
130
- open,
131
- setOpen,
132
- isMobile,
133
- openMobile,
134
- setOpenMobile,
135
- toggleSidebar,
136
- }),
137
- [state, open, setOpen, isMobile, openMobile, toggleSidebar],
138
- );
139
-
140
- return (
141
- <SidebarContext.Provider value={contextValue}>
142
- <TooltipProvider delayDuration={0}>
143
- <div
144
- style={
145
- {
146
- "--sidebar-width": SIDEBAR_WIDTH,
147
- "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
148
- ...style,
149
- } as React.CSSProperties
150
- }
151
- className={cn(
152
- "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
153
- className,
154
- )}
155
- ref={ref}
156
- {...props}
157
- >
158
- {children}
159
- </div>
160
- </TooltipProvider>
161
- </SidebarContext.Provider>
162
- );
163
- },
164
- );
165
- SidebarProvider.displayName = "SidebarProvider";
166
-
167
- export interface SidebarProps extends React.ComponentProps<"div"> {
168
- /**
169
- * Which side of the layout the sidebar lives on.
170
- * @default "left"
171
- */
172
- side?: "left" | "right";
173
- /**
174
- * `sidebar` is the standard layout column, `floating` lifts the sidebar
175
- * with a shadow, `inset` insets it into the page surface.
176
- * @default "sidebar"
177
- */
178
- variant?: "sidebar" | "floating" | "inset";
179
- /**
180
- * Collapse mode. `offcanvas` slides off-screen, `icon` collapses to
181
- * icons-only, `none` disables collapsing.
182
- * @default "offcanvas"
183
- */
184
- collapsible?: "offcanvas" | "icon" | "none";
185
- /** Header + Content + Footer. */
186
- children?: React.ReactNode;
187
- className?: string;
188
- }
189
-
190
- /**
191
- * The sidebar shell — chooses left/right side, `sidebar`/`floating`/`inset`
192
- * variant, and the collapse mode. Renders a Sheet drawer instead of an inline
193
- * column when `useIsMobile()` returns true.
194
- */
195
- const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
196
- (
197
- {
198
- side = "left",
199
- variant = "sidebar",
200
- collapsible = "offcanvas",
201
- className,
202
- children,
203
- ...props
204
- },
205
- ref,
206
- ) => {
207
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
208
-
209
- if (collapsible === "none") {
210
- return (
211
- <div
212
- data-slot="sidebar"
213
- className={cn(
214
- "flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground",
215
- className,
216
- )}
217
- ref={ref}
218
- {...props}
219
- >
220
- {children}
221
- </div>
222
- );
223
- }
224
-
225
- if (isMobile) {
226
- return (
227
- <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
228
- <SheetContent
229
- data-sidebar="sidebar"
230
- data-mobile="true"
231
- className="w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
232
- style={
233
- {
234
- "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
235
- } as React.CSSProperties
236
- }
237
- side={side}
238
- >
239
- <SheetHeader className="sr-only">
240
- <SheetTitle>Sidebar</SheetTitle>
241
- <SheetDescription>Displays the mobile sidebar.</SheetDescription>
242
- </SheetHeader>
243
- <div className="flex h-full w-full flex-col">{children}</div>
244
- </SheetContent>
245
- </Sheet>
246
- );
247
- }
248
-
249
- return (
250
- <div
251
- ref={ref}
252
- className="group peer hidden text-sidebar-foreground md:block"
253
- data-state={state}
254
- data-collapsible={state === "collapsed" ? collapsible : ""}
255
- data-variant={variant}
256
- data-side={side}
257
- >
258
- {/* This is what handles the sidebar gap on desktop */}
259
- <div
260
- className={cn(
261
- "relative w-[var(--sidebar-width)] bg-transparent transition-[width] duration-200 ease-linear",
262
- "group-data-[collapsible=offcanvas]:w-0",
263
- "group-data-[side=right]:rotate-180",
264
- variant === "floating" || variant === "inset"
265
- ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
266
- : "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]",
267
- )}
268
- />
269
- <div
270
- className={cn(
271
- "fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] duration-200 ease-linear md:flex",
272
- side === "left"
273
- ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
274
- : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
275
- // Adjust the padding for floating and inset variants.
276
- variant === "floating" || variant === "inset"
277
- ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
278
- : "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l",
279
- className,
280
- )}
281
- {...props}
282
- >
283
- <div
284
- data-sidebar="sidebar"
285
- data-slot="sidebar"
286
- className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
287
- >
288
- {children}
289
- </div>
290
- </div>
291
- </div>
292
- );
293
- },
294
- );
295
- Sidebar.displayName = "Sidebar";
296
-
297
- export interface SidebarTriggerProps extends React.ComponentProps<typeof Button> {
298
- /**
299
- * Mirrors `Button` variants. Defaults to ghost for low-emphasis chrome.
300
- * @default "ghost"
301
- */
302
- variant?: React.ComponentProps<typeof Button>["variant"];
303
- /**
304
- * Mirrors `Button` sizes. Defaults to icon.
305
- * @default "icon"
306
- */
307
- size?: React.ComponentProps<typeof Button>["size"];
308
- /** Click handler chained before `toggleSidebar()`. */
309
- onClick?: React.MouseEventHandler<HTMLButtonElement>;
310
- className?: string;
311
- }
312
-
313
- /**
314
- * Default button that toggles the sidebar via `useSidebar().toggleSidebar`.
315
- * Renders the panel-left glyph; swap with your own button if you want a
316
- * different icon (e.g. a hamburger).
317
- */
318
- const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, SidebarTriggerProps>(
319
- ({ className, onClick, ...props }, ref) => {
320
- const { toggleSidebar } = useSidebar();
321
-
322
- return (
323
- <Button
324
- ref={ref}
325
- data-sidebar="trigger"
326
- variant="ghost"
327
- size="icon"
328
- className={cn("h-7 w-7", className)}
329
- onClick={(event) => {
330
- onClick?.(event);
331
- toggleSidebar();
332
- }}
333
- {...props}
334
- >
335
- <PanelLeft />
336
- <span className="sr-only">Toggle Sidebar</span>
337
- </Button>
338
- );
339
- },
340
- );
341
- SidebarTrigger.displayName = "SidebarTrigger";
342
-
343
- export interface SidebarRailProps extends React.ComponentProps<"button"> {
344
- className?: string;
345
- }
346
-
347
- /**
348
- * Thin draggable rail at the sidebar's edge that toggles open/collapsed.
349
- * Optional — only useful when `collapsible !== "none"`.
350
- */
351
- const SidebarRail = React.forwardRef<HTMLButtonElement, SidebarRailProps>(
352
- ({ className, ...props }, ref) => {
353
- const { toggleSidebar } = useSidebar();
354
-
355
- return (
356
- <button
357
- ref={ref}
358
- data-sidebar="rail"
359
- aria-label="Toggle Sidebar"
360
- tabIndex={-1}
361
- onClick={toggleSidebar}
362
- title="Toggle Sidebar"
363
- className={cn(
364
- "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
365
- "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
366
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
367
- "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
368
- "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
369
- "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
370
- className,
371
- )}
372
- {...props}
373
- />
374
- );
375
- },
376
- );
377
- SidebarRail.displayName = "SidebarRail";
378
-
379
- export interface SidebarInsetProps extends React.ComponentProps<"main"> {
380
- className?: string;
381
- children?: React.ReactNode;
382
- }
383
-
384
- /**
385
- * The main content column — sibling of `<Sidebar>`. Adapts margin /
386
- * border-radius / shadow when the sidebar uses `variant="inset"`. Place your
387
- * topbar + page content inside.
388
- */
389
- const SidebarInset = React.forwardRef<HTMLDivElement, SidebarInsetProps>(
390
- ({ className, ...props }, ref) => {
391
- return (
392
- <main
393
- ref={ref}
394
- data-slot="sidebar-inset"
395
- className={cn(
396
- "relative flex w-full flex-1 flex-col bg-background",
397
- "md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
398
- className,
399
- )}
400
- {...props}
401
- />
402
- );
403
- },
404
- );
405
- SidebarInset.displayName = "SidebarInset";
406
-
407
- export interface SidebarInputProps extends React.ComponentProps<typeof Input> {
408
- className?: string;
409
- }
410
-
411
- /**
412
- * `<Input>` styled to fit inside the sidebar (transparent bg, no shadow,
413
- * sidebar-ring focus colour). Use for an in-sidebar search field.
414
- */
415
- const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, SidebarInputProps>(
416
- ({ className, ...props }, ref) => {
417
- return (
418
- <Input
419
- ref={ref}
420
- data-sidebar="input"
421
- className={cn(
422
- "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
423
- className,
424
- )}
425
- {...props}
426
- />
427
- );
428
- },
429
- );
430
- SidebarInput.displayName = "SidebarInput";
431
-
432
- export interface SidebarHeaderProps extends React.ComponentProps<"div"> {
433
- className?: string;
434
- children?: React.ReactNode;
435
- }
436
-
437
- /**
438
- * Top region of the sidebar. Default styling: 56px tall, row flex, gap-2.5,
439
- * 16px horizontal padding, bottom border. Centred when collapsed to the icon
440
- * rail. Typically holds the brand mark and a collapse trigger.
441
- */
442
- const SidebarHeader = React.forwardRef<HTMLDivElement, SidebarHeaderProps>(
443
- ({ className, ...props }, ref) => {
444
- return (
445
- <div
446
- ref={ref}
447
- data-sidebar="header"
448
- className={cn(
449
- "flex h-14 shrink-0 items-center gap-2.5 border-b border-sidebar-border px-4 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0",
450
- className,
451
- )}
452
- {...props}
453
- />
454
- );
455
- },
456
- );
457
- SidebarHeader.displayName = "SidebarHeader";
458
-
459
- export interface SidebarFooterProps extends React.ComponentProps<"div"> {
460
- className?: string;
461
- children?: React.ReactNode;
462
- }
463
-
464
- /**
465
- * Bottom region of the sidebar — default styling: column flex with 8px gap
466
- * and 8px padding. Common uses: a sign-out button, version string, or theme
467
- * toggle.
468
- */
469
- const SidebarFooter = React.forwardRef<HTMLDivElement, SidebarFooterProps>(
470
- ({ className, ...props }, ref) => {
471
- return (
472
- <div
473
- ref={ref}
474
- data-sidebar="footer"
475
- className={cn("flex flex-col gap-2 p-2", className)}
476
- {...props}
477
- />
478
- );
479
- },
480
- );
481
- SidebarFooter.displayName = "SidebarFooter";
482
-
483
- export interface SidebarSeparatorProps extends React.ComponentProps<typeof Separator> {
484
- className?: string;
485
- }
486
-
487
- /**
488
- * Horizontal divider with `mx-2` and the sidebar-border colour. Drop between
489
- * groups when you want a visible split (otherwise the gap-2 spacing on
490
- * `SidebarContent` is usually enough).
491
- */
492
- const SidebarSeparator = React.forwardRef<
493
- React.ElementRef<typeof Separator>,
494
- SidebarSeparatorProps
495
- >(({ className, ...props }, ref) => {
496
- return (
497
- <Separator
498
- ref={ref}
499
- data-sidebar="separator"
500
- className={cn("mx-2 w-auto bg-sidebar-border", className)}
501
- {...props}
502
- />
503
- );
504
- });
505
- SidebarSeparator.displayName = "SidebarSeparator";
506
-
507
- export interface SidebarContentProps extends React.ComponentProps<"div"> {
508
- children?: React.ReactNode;
509
- className?: string;
510
- }
511
-
512
- /**
513
- * Scrollable region between header and footer. Wrap one or more
514
- * `<SidebarGroup>`s here. Uses canvas's `<ScrollArea>` so the scrollbar
515
- * matches the rest of the design system.
516
- */
517
- const SidebarContent = React.forwardRef<HTMLDivElement, SidebarContentProps>(
518
- ({ className, children, ...props }, ref) => {
519
- return (
520
- <ScrollArea data-sidebar="content" className={cn("flex min-h-0 flex-1 flex-col", className)}>
521
- <div
522
- ref={ref}
523
- className="flex flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden"
524
- {...props}
525
- >
526
- {children}
527
- </div>
528
- </ScrollArea>
529
- );
530
- },
531
- );
532
- SidebarContent.displayName = "SidebarContent";
533
-
534
- export interface SidebarGroupProps extends React.ComponentProps<"div"> {
535
- className?: string;
536
- children?: React.ReactNode;
537
- }
538
-
539
- /**
540
- * Section wrapper inside `SidebarContent`. Use one per logical chunk of nav
541
- * (Overview, Identity, OAuth2, …). Pair with `SidebarGroupLabel` +
542
- * `SidebarGroupContent`.
543
- */
544
- const SidebarGroup = React.forwardRef<HTMLDivElement, SidebarGroupProps>(
545
- ({ className, ...props }, ref) => {
546
- return (
547
- <div
548
- ref={ref}
549
- data-sidebar="group"
550
- className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
551
- {...props}
552
- />
553
- );
554
- },
555
- );
556
- SidebarGroup.displayName = "SidebarGroup";
557
-
558
- export interface SidebarGroupLabelProps extends React.ComponentProps<"div"> {
559
- /**
560
- * Render as a Radix Slot — wrap a custom element while inheriting the
561
- * label styling.
562
- * @default false
563
- */
564
- asChild?: boolean;
565
- className?: string;
566
- children?: React.ReactNode;
567
- }
568
-
569
- /**
570
- * Small uppercase heading at the top of a group. 11px, tracking-wider,
571
- * sidebar-foreground/70. Auto-hides when the sidebar collapses to the icon
572
- * rail.
573
- */
574
- const SidebarGroupLabel = React.forwardRef<HTMLDivElement, SidebarGroupLabelProps>(
575
- ({ className, asChild = false, ...props }, ref) => {
576
- const Comp = asChild ? Slot : "div";
577
-
578
- return (
579
- <Comp
580
- ref={ref}
581
- data-sidebar="group-label"
582
- className={cn(
583
- "flex h-8 shrink-0 items-center rounded-md px-2.5 text-[11px] font-medium uppercase tracking-wider text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
584
- "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
585
- className,
586
- )}
587
- {...props}
588
- />
589
- );
590
- },
591
- );
592
- SidebarGroupLabel.displayName = "SidebarGroupLabel";
593
-
594
- export interface SidebarGroupActionProps extends React.ComponentProps<"button"> {
595
- /**
596
- * Render as a Radix Slot — wrap a custom button element.
597
- * @default false
598
- */
599
- asChild?: boolean;
600
- className?: string;
601
- children?: React.ReactNode;
602
- }
603
-
604
- /**
605
- * Right-aligned button inside a `SidebarGroup` (e.g. a `+` to add an item to
606
- * that section). Hidden when the sidebar collapses to the icon rail.
607
- */
608
- const SidebarGroupAction = React.forwardRef<HTMLButtonElement, SidebarGroupActionProps>(
609
- ({ className, asChild = false, ...props }, ref) => {
610
- const Comp = asChild ? Slot : "button";
611
-
612
- return (
613
- <Comp
614
- ref={ref}
615
- data-sidebar="group-action"
616
- className={cn(
617
- "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
618
- // Increases the hit area of the button on mobile.
619
- "after:absolute after:-inset-2 after:md:hidden",
620
- "group-data-[collapsible=icon]:hidden",
621
- className,
622
- )}
623
- {...props}
624
- />
625
- );
626
- },
627
- );
628
- SidebarGroupAction.displayName = "SidebarGroupAction";
629
-
630
- export interface SidebarGroupContentProps extends React.ComponentProps<"div"> {
631
- className?: string;
632
- children?: React.ReactNode;
633
- }
634
-
635
- /**
636
- * Inner content slot of a `SidebarGroup`. Holds a `SidebarMenu` (or any
637
- * custom UI you want grouped under the label).
638
- */
639
- const SidebarGroupContent = React.forwardRef<HTMLDivElement, SidebarGroupContentProps>(
640
- ({ className, ...props }, ref) => (
641
- <div
642
- ref={ref}
643
- data-sidebar="group-content"
644
- className={cn("w-full text-sm", className)}
645
- {...props}
646
- />
647
- ),
648
- );
649
- SidebarGroupContent.displayName = "SidebarGroupContent";
650
-
651
- export interface SidebarMenuProps extends React.ComponentProps<"ul"> {
652
- className?: string;
653
- children?: React.ReactNode;
654
- }
655
-
656
- /**
657
- * Unordered list (`<ul>`) of `SidebarMenuItem`s. The list semantics are
658
- * important for screen-reader users — keep menu items in here rather than
659
- * inlining buttons in the group.
660
- */
661
- const SidebarMenu = React.forwardRef<HTMLUListElement, SidebarMenuProps>(
662
- ({ className, ...props }, ref) => (
663
- <ul
664
- ref={ref}
665
- data-sidebar="menu"
666
- className={cn("flex w-full min-w-0 flex-col gap-1", className)}
667
- {...props}
668
- />
669
- ),
670
- );
671
- SidebarMenu.displayName = "SidebarMenu";
672
-
673
- export interface SidebarMenuItemProps extends React.ComponentProps<"li"> {
674
- className?: string;
675
- children?: React.ReactNode;
676
- }
677
-
678
- /**
679
- * Single row in the menu (`<li>`). Wraps a `SidebarMenuButton` plus an
680
- * optional `SidebarMenuAction` and `SidebarMenuBadge`.
681
- */
682
- const SidebarMenuItem = React.forwardRef<HTMLLIElement, SidebarMenuItemProps>(
683
- ({ className, ...props }, ref) => (
684
- <li
685
- ref={ref}
686
- data-sidebar="menu-item"
687
- className={cn("group/menu-item relative", className)}
688
- {...props}
689
- />
690
- ),
691
- );
692
- SidebarMenuItem.displayName = "SidebarMenuItem";
693
-
694
- const sidebarMenuButtonVariants = cva(
695
- "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
696
- {
697
- variants: {
698
- variant: {
699
- default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
700
- outline:
701
- "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
702
- },
703
- size: {
704
- default: "h-8 text-sm",
705
- sm: "h-7 text-xs",
706
- lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
707
- },
708
- },
709
- defaultVariants: {
710
- variant: "default",
711
- size: "default",
712
- },
713
- },
714
- );
715
-
716
- export interface SidebarMenuButtonProps
717
- extends React.ComponentProps<"button">,
718
- VariantProps<typeof sidebarMenuButtonVariants> {
719
- /**
720
- * Render as a Radix Slot — wrap a router `<Link>` to use the menu item
721
- * as navigation.
722
- * @default false
723
- */
724
- asChild?: boolean;
725
- /**
726
- * Mark as the currently active item (highlighted styling).
727
- * @default false
728
- */
729
- isActive?: boolean;
730
- /**
731
- * Tooltip text shown when the sidebar collapses to icons-only mode (so
732
- * the label is still discoverable).
733
- */
734
- tooltip?: string | React.ComponentProps<typeof TooltipContent>;
735
- /**
736
- * Visual style.
737
- * @default "default"
738
- */
739
- variant?: "default" | "outline";
740
- /**
741
- * Height preset.
742
- * @default "default"
743
- */
744
- size?: "default" | "sm" | "lg";
745
- className?: string;
746
- children?: React.ReactNode;
747
- }
748
-
749
- /**
750
- * The clickable nav item itself — supports `isActive`, `tooltip` (shown when
751
- * collapsed to the rail), and `variant`/`size`. Use `asChild` to wrap a
752
- * router `<Link>` instead of rendering a `<button>`.
753
- */
754
- const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(
755
- (
756
- {
757
- asChild = false,
758
- isActive = false,
759
- variant = "default",
760
- size = "default",
761
- tooltip,
762
- className,
763
- ...props
764
- },
765
- ref,
766
- ) => {
767
- const Comp = asChild ? Slot : "button";
768
- const { isMobile, state } = useSidebar();
769
-
770
- const button = (
771
- <Comp
772
- ref={ref}
773
- data-sidebar="menu-button"
774
- data-size={size}
775
- data-active={isActive}
776
- className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
777
- {...props}
778
- />
779
- );
780
-
781
- if (!tooltip) {
782
- return button;
783
- }
784
-
785
- if (typeof tooltip === "string") {
786
- tooltip = {
787
- children: tooltip,
788
- };
789
- }
790
-
791
- return (
792
- <Tooltip>
793
- <TooltipTrigger asChild>{button}</TooltipTrigger>
794
- <TooltipContent
795
- side="right"
796
- align="center"
797
- hidden={state !== "collapsed" || isMobile}
798
- {...tooltip}
799
- />
800
- </Tooltip>
801
- );
802
- },
803
- );
804
- SidebarMenuButton.displayName = "SidebarMenuButton";
805
-
806
- export interface SidebarMenuActionProps extends React.ComponentProps<"button"> {
807
- /**
808
- * Render as a Radix Slot — wrap a custom button element.
809
- * @default false
810
- */
811
- asChild?: boolean;
812
- /**
813
- * Only reveal the action when the parent menu item is hovered or
814
- * focused.
815
- * @default false
816
- */
817
- showOnHover?: boolean;
818
- className?: string;
819
- children?: React.ReactNode;
820
- }
821
-
822
- /**
823
- * Icon button anchored to the right of a `SidebarMenuItem` (e.g. a row's
824
- * overflow menu). Use `showOnHover` to keep it hidden until the row is
825
- * hovered/focused.
826
- */
827
- const SidebarMenuAction = React.forwardRef<HTMLButtonElement, SidebarMenuActionProps>(
828
- ({ className, asChild = false, showOnHover = false, ...props }, ref) => {
829
- const Comp = asChild ? Slot : "button";
830
-
831
- return (
832
- <Comp
833
- ref={ref}
834
- data-sidebar="menu-action"
835
- className={cn(
836
- "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
837
- // Increases the hit area of the button on mobile.
838
- "after:absolute after:-inset-2 after:md:hidden",
839
- "peer-data-[size=sm]/menu-button:top-1",
840
- "peer-data-[size=default]/menu-button:top-1.5",
841
- "peer-data-[size=lg]/menu-button:top-2.5",
842
- "group-data-[collapsible=icon]:hidden",
843
- showOnHover &&
844
- "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
845
- className,
846
- )}
847
- {...props}
848
- />
849
- );
850
- },
851
- );
852
- SidebarMenuAction.displayName = "SidebarMenuAction";
853
-
854
- export interface SidebarMenuBadgeProps extends React.ComponentProps<"div"> {
855
- className?: string;
856
- children?: React.ReactNode;
857
- }
858
-
859
- /**
860
- * Right-aligned numeric or status badge inside a menu item (e.g. unread
861
- * count, pending count). Hidden when the sidebar collapses to the icon rail.
862
- */
863
- const SidebarMenuBadge = React.forwardRef<HTMLDivElement, SidebarMenuBadgeProps>(
864
- ({ className, ...props }, ref) => (
865
- <div
866
- ref={ref}
867
- data-sidebar="menu-badge"
868
- className={cn(
869
- "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
870
- "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
871
- "peer-data-[size=sm]/menu-button:top-1",
872
- "peer-data-[size=default]/menu-button:top-1.5",
873
- "peer-data-[size=lg]/menu-button:top-2.5",
874
- "group-data-[collapsible=icon]:hidden",
875
- className,
876
- )}
877
- {...props}
878
- />
879
- ),
880
- );
881
- SidebarMenuBadge.displayName = "SidebarMenuBadge";
882
-
883
- export interface SidebarMenuSkeletonProps extends React.ComponentProps<"div"> {
884
- /**
885
- * Render a skeleton block where the leading icon would normally be.
886
- * @default false
887
- */
888
- showIcon?: boolean;
889
- className?: string;
890
- }
891
-
892
- /**
893
- * Loading-state placeholder that matches the `SidebarMenuButton` height. Set
894
- * `showIcon` to also render a leading icon placeholder.
895
- */
896
- const SidebarMenuSkeleton = React.forwardRef<HTMLDivElement, SidebarMenuSkeletonProps>(
897
- ({ className, showIcon = false, ...props }, ref) => {
898
- // Random width between 50 to 90%.
899
- const width = React.useMemo(() => {
900
- return `${Math.floor(Math.random() * 40) + 50}%`;
901
- }, []);
902
-
903
- return (
904
- <div
905
- ref={ref}
906
- data-sidebar="menu-skeleton"
907
- className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
908
- {...props}
909
- >
910
- {showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
911
- <Skeleton
912
- className="h-4 max-w-[var(--skeleton-width)] flex-1"
913
- data-sidebar="menu-skeleton-text"
914
- style={
915
- {
916
- "--skeleton-width": width,
917
- } as React.CSSProperties
918
- }
919
- />
920
- </div>
921
- );
922
- },
923
- );
924
- SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
925
-
926
- export interface SidebarMenuSubProps extends React.ComponentProps<"ul"> {
927
- className?: string;
928
- children?: React.ReactNode;
929
- }
930
-
931
- /**
932
- * Nested menu (`<ul>`) for sub-items under a `SidebarMenuButton`. Pair with
933
- * `SidebarMenuSubItem` + `SidebarMenuSubButton`.
934
- */
935
- const SidebarMenuSub = React.forwardRef<HTMLUListElement, SidebarMenuSubProps>(
936
- ({ className, ...props }, ref) => (
937
- <ul
938
- ref={ref}
939
- data-sidebar="menu-sub"
940
- className={cn(
941
- "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
942
- "group-data-[collapsible=icon]:hidden",
943
- className,
944
- )}
945
- {...props}
946
- />
947
- ),
948
- );
949
- SidebarMenuSub.displayName = "SidebarMenuSub";
950
-
951
- export interface SidebarMenuSubItemProps extends React.ComponentProps<"li"> {
952
- className?: string;
953
- children?: React.ReactNode;
954
- }
955
-
956
- /**
957
- * `<li>` wrapper for an item inside a `SidebarMenuSub`. Matches
958
- * `SidebarMenuItem` semantically but at the sub level.
959
- */
960
- const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, SidebarMenuSubItemProps>(
961
- ({ ...props }, ref) => <li ref={ref} {...props} />,
962
- );
963
- SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
964
-
965
- export interface SidebarMenuSubButtonProps extends React.ComponentProps<"a"> {
966
- /**
967
- * Render as a Radix Slot — wrap a router `<Link>` instead of `<a>`.
968
- * @default false
969
- */
970
- asChild?: boolean;
971
- /**
972
- * `sm` or `md` — sub-items are typically smaller than top-level menu
973
- * buttons.
974
- * @default "md"
975
- */
976
- size?: "sm" | "md";
977
- /**
978
- * Mark as the currently active sub-item.
979
- * @default false
980
- */
981
- isActive?: boolean;
982
- className?: string;
983
- children?: React.ReactNode;
984
- }
985
-
986
- /**
987
- * The clickable element inside a `SidebarMenuSubItem`. Defaults to an `<a>`
988
- * (use `asChild` for router links). Smaller than top-level menu buttons.
989
- */
990
- const SidebarMenuSubButton = React.forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(
991
- ({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
992
- const Comp = asChild ? Slot : "a";
993
-
994
- return (
995
- <Comp
996
- ref={ref}
997
- data-sidebar="menu-sub-button"
998
- data-size={size}
999
- data-active={isActive}
1000
- className={cn(
1001
- "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
1002
- "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
1003
- size === "sm" && "text-xs",
1004
- size === "md" && "text-sm",
1005
- "group-data-[collapsible=icon]:hidden",
1006
- className,
1007
- )}
1008
- {...props}
1009
- />
1010
- );
1011
- },
1012
- );
1013
- SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
1014
-
1015
- export {
1016
- Sidebar,
1017
- SidebarContent,
1018
- SidebarFooter,
1019
- SidebarGroup,
1020
- SidebarGroupAction,
1021
- SidebarGroupContent,
1022
- SidebarGroupLabel,
1023
- SidebarHeader,
1024
- SidebarInput,
1025
- SidebarInset,
1026
- SidebarMenu,
1027
- SidebarMenuAction,
1028
- SidebarMenuBadge,
1029
- SidebarMenuButton,
1030
- SidebarMenuItem,
1031
- SidebarMenuSkeleton,
1032
- SidebarMenuSub,
1033
- SidebarMenuSubButton,
1034
- SidebarMenuSubItem,
1035
- SidebarProvider,
1036
- SidebarRail,
1037
- SidebarSeparator,
1038
- SidebarTrigger,
1039
- useSidebar,
1040
- };