@teamix-evo/ui 0.1.1 → 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.
Files changed (295) hide show
  1. package/README.md +184 -184
  2. package/manifest.json +680 -492
  3. package/package.json +20 -10
  4. package/src/components/accordion/accordion.meta.md +5 -4
  5. package/src/components/accordion/accordion.stories.tsx +14 -9
  6. package/src/components/accordion/accordion.tsx +104 -8
  7. package/src/components/affix/affix.meta.md +20 -2
  8. package/src/components/affix/affix.stories.tsx +102 -25
  9. package/src/components/affix/affix.tsx +79 -9
  10. package/src/components/alert/alert.meta.md +44 -13
  11. package/src/components/alert/alert.stories.tsx +66 -21
  12. package/src/components/alert/alert.tsx +81 -34
  13. package/src/components/alert-dialog/alert-dialog.meta.md +61 -16
  14. package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
  15. package/src/components/alert-dialog/alert-dialog.tsx +60 -13
  16. package/src/components/anchor/anchor.meta.md +8 -3
  17. package/src/components/anchor/anchor.stories.tsx +3 -3
  18. package/src/components/anchor/anchor.tsx +2 -2
  19. package/src/components/app/app.meta.md +9 -4
  20. package/src/components/app/app.stories.tsx +9 -7
  21. package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -3
  22. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
  23. package/src/components/auto-complete/auto-complete.meta.md +14 -6
  24. package/src/components/auto-complete/auto-complete.stories.tsx +47 -4
  25. package/src/components/auto-complete/auto-complete.tsx +119 -71
  26. package/src/components/avatar/avatar.meta.md +6 -7
  27. package/src/components/avatar/avatar.stories.tsx +21 -3
  28. package/src/components/avatar/avatar.tsx +24 -23
  29. package/src/components/badge/badge.meta.md +10 -9
  30. package/src/components/badge/badge.stories.tsx +2 -2
  31. package/src/components/badge/badge.tsx +9 -15
  32. package/src/components/breadcrumb/breadcrumb.meta.md +27 -7
  33. package/src/components/breadcrumb/breadcrumb.stories.tsx +127 -4
  34. package/src/components/breadcrumb/breadcrumb.tsx +22 -8
  35. package/src/components/button/button.meta.md +258 -21
  36. package/src/components/button/button.stories.tsx +549 -41
  37. package/src/components/button/button.tsx +335 -33
  38. package/src/components/button/demo/as-child.tsx +24 -0
  39. package/src/components/button/demo/basic.tsx +8 -0
  40. package/src/components/button/demo/block.tsx +16 -0
  41. package/src/components/button/demo/loading.tsx +19 -0
  42. package/src/components/button/demo/shapes.tsx +18 -0
  43. package/src/components/button/demo/sizes.tsx +19 -0
  44. package/src/components/button/demo/variants.tsx +19 -0
  45. package/src/components/button/demo/with-icon.tsx +20 -0
  46. package/src/components/calendar/calendar.meta.md +13 -3
  47. package/src/components/calendar/calendar.stories.tsx +6 -6
  48. package/src/components/calendar/calendar.tsx +73 -8
  49. package/src/components/card/card.meta.md +27 -5
  50. package/src/components/card/card.stories.tsx +42 -3
  51. package/src/components/card/card.tsx +146 -63
  52. package/src/components/carousel/carousel.meta.md +4 -3
  53. package/src/components/carousel/carousel.stories.tsx +11 -6
  54. package/src/components/cascader/cascader.meta.md +47 -17
  55. package/src/components/cascader/cascader.stories.tsx +22 -10
  56. package/src/components/cascader/cascader.tsx +428 -85
  57. package/src/components/checkbox/checkbox.meta.md +75 -7
  58. package/src/components/checkbox/checkbox.stories.tsx +161 -3
  59. package/src/components/checkbox/checkbox.tsx +77 -9
  60. package/src/components/collapsible/collapsible.meta.md +14 -6
  61. package/src/components/collapsible/collapsible.stories.tsx +10 -2
  62. package/src/components/collapsible/collapsible.tsx +93 -6
  63. package/src/components/color-picker/color-picker.meta.md +12 -7
  64. package/src/components/color-picker/color-picker.stories.tsx +86 -7
  65. package/src/components/color-picker/color-picker.tsx +20 -9
  66. package/src/components/command/command.meta.md +29 -13
  67. package/src/components/command/command.stories.tsx +4 -4
  68. package/src/components/command/command.tsx +19 -8
  69. package/src/components/context-menu/context-menu.meta.md +11 -8
  70. package/src/components/context-menu/context-menu.stories.tsx +11 -3
  71. package/src/components/context-menu/context-menu.tsx +21 -8
  72. package/src/components/data-table/data-table.meta.md +6 -5
  73. package/src/components/data-table/data-table.stories.tsx +13 -6
  74. package/src/components/data-table/data-table.tsx +2 -2
  75. package/src/components/date-picker/date-picker.meta.md +88 -19
  76. package/src/components/date-picker/date-picker.stories.tsx +55 -5
  77. package/src/components/date-picker/date-picker.tsx +1489 -91
  78. package/src/components/descriptions/descriptions.meta.md +10 -5
  79. package/src/components/descriptions/descriptions.stories.tsx +3 -3
  80. package/src/components/descriptions/descriptions.tsx +22 -14
  81. package/src/components/dialog/dialog.meta.md +76 -13
  82. package/src/components/dialog/dialog.stories.tsx +182 -20
  83. package/src/components/dialog/dialog.tsx +67 -15
  84. package/src/components/dialog/imperative.tsx +252 -0
  85. package/src/components/drawer/drawer.meta.md +33 -34
  86. package/src/components/drawer/drawer.stories.tsx +29 -12
  87. package/src/components/drawer/drawer.tsx +22 -113
  88. package/src/components/dropdown-menu/dropdown-menu.meta.md +78 -10
  89. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +88 -2
  90. package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
  91. package/src/components/ellipsis/ellipsis.meta.md +87 -0
  92. package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
  93. package/src/components/ellipsis/ellipsis.tsx +153 -0
  94. package/src/components/empty/empty.meta.md +9 -4
  95. package/src/components/empty/empty.stories.tsx +4 -4
  96. package/src/components/empty/empty.tsx +10 -3
  97. package/src/components/field/field.meta.md +47 -9
  98. package/src/components/field/field.stories.tsx +385 -5
  99. package/src/components/field/field.tsx +263 -35
  100. package/src/components/filter-bar/filter-bar.meta.md +92 -0
  101. package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
  102. package/src/components/filter-bar/filter-bar.tsx +568 -0
  103. package/src/components/flex/flex.meta.md +54 -6
  104. package/src/components/flex/flex.stories.tsx +107 -20
  105. package/src/components/flex/flex.tsx +27 -4
  106. package/src/components/float-button/float-button.meta.md +8 -3
  107. package/src/components/float-button/float-button.stories.tsx +9 -7
  108. package/src/components/float-button/float-button.tsx +1 -1
  109. package/src/components/form/form.meta.md +39 -17
  110. package/src/components/form/form.stories.tsx +350 -3
  111. package/src/components/form/form.tsx +101 -35
  112. package/src/components/grid/grid.meta.md +7 -2
  113. package/src/components/grid/grid.stories.tsx +6 -4
  114. package/src/components/hover-card/hover-card.meta.md +20 -9
  115. package/src/components/hover-card/hover-card.stories.tsx +34 -5
  116. package/src/components/hover-card/hover-card.tsx +51 -13
  117. package/src/components/icon/DEVELOPMENT.md +809 -0
  118. package/src/components/icon/icon.meta.md +170 -0
  119. package/src/components/icon/icon.stories.tsx +344 -0
  120. package/src/components/icon/icon.tsx +248 -0
  121. package/src/components/image/image.meta.md +9 -4
  122. package/src/components/image/image.stories.tsx +3 -3
  123. package/src/components/image/image.tsx +6 -4
  124. package/src/components/input/demo/basic.tsx +12 -0
  125. package/src/components/input/demo/clearable.tsx +21 -0
  126. package/src/components/input/demo/show-count.tsx +18 -0
  127. package/src/components/input/demo/sizes.tsx +15 -0
  128. package/src/components/input/input.meta.md +39 -33
  129. package/src/components/input/input.stories.tsx +62 -35
  130. package/src/components/input/input.tsx +97 -98
  131. package/src/components/input-group/input-group.meta.md +54 -22
  132. package/src/components/input-group/input-group.stories.tsx +49 -16
  133. package/src/components/input-group/input-group.tsx +44 -8
  134. package/src/components/input-number/input-number.meta.md +64 -7
  135. package/src/components/input-number/input-number.stories.tsx +46 -8
  136. package/src/components/input-number/input-number.tsx +99 -26
  137. package/src/components/input-otp/input-otp.meta.md +4 -3
  138. package/src/components/input-otp/input-otp.stories.tsx +3 -3
  139. package/src/components/input-otp/input-otp.tsx +1 -1
  140. package/src/components/item/item.meta.md +8 -3
  141. package/src/components/item/item.stories.tsx +8 -5
  142. package/src/components/item/item.tsx +7 -6
  143. package/src/components/kbd/kbd.meta.md +13 -4
  144. package/src/components/kbd/kbd.stories.tsx +4 -4
  145. package/src/components/kbd/kbd.tsx +10 -5
  146. package/src/components/label/label.meta.md +18 -10
  147. package/src/components/label/label.stories.tsx +64 -6
  148. package/src/components/label/label.tsx +91 -19
  149. package/src/components/masonry/masonry.meta.md +8 -3
  150. package/src/components/masonry/masonry.stories.tsx +7 -5
  151. package/src/components/masonry/masonry.tsx +1 -0
  152. package/src/components/mentions/mentions.meta.md +36 -6
  153. package/src/components/mentions/mentions.stories.tsx +120 -6
  154. package/src/components/mentions/mentions.tsx +11 -5
  155. package/src/components/menubar/menubar.meta.md +30 -12
  156. package/src/components/menubar/menubar.stories.tsx +62 -2
  157. package/src/components/menubar/menubar.tsx +9 -9
  158. package/src/components/native-select/native-select.meta.md +8 -3
  159. package/src/components/native-select/native-select.stories.tsx +8 -5
  160. package/src/components/native-select/native-select.tsx +1 -1
  161. package/src/components/navigation-menu/navigation-menu.meta.md +19 -9
  162. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -9
  163. package/src/components/navigation-menu/navigation-menu.tsx +8 -4
  164. package/src/components/notification/notification.meta.md +52 -10
  165. package/src/components/notification/notification.stories.tsx +11 -9
  166. package/src/components/notification/notification.tsx +36 -21
  167. package/src/components/page-header/DEVELOPMENT.md +842 -0
  168. package/src/components/page-header/page-header.meta.md +208 -0
  169. package/src/components/page-header/page-header.stories.tsx +421 -0
  170. package/src/components/page-header/page-header.tsx +281 -0
  171. package/src/components/pagination/pagination.meta.md +140 -37
  172. package/src/components/pagination/pagination.stories.tsx +232 -10
  173. package/src/components/pagination/pagination.tsx +355 -63
  174. package/src/components/popconfirm/popconfirm.meta.md +9 -4
  175. package/src/components/popconfirm/popconfirm.stories.tsx +3 -4
  176. package/src/components/popconfirm/popconfirm.tsx +2 -2
  177. package/src/components/popover/popover.meta.md +62 -5
  178. package/src/components/popover/popover.stories.tsx +83 -7
  179. package/src/components/popover/popover.tsx +77 -28
  180. package/src/components/progress/progress.meta.md +38 -6
  181. package/src/components/progress/progress.stories.tsx +3 -3
  182. package/src/components/progress/progress.tsx +24 -16
  183. package/src/components/radio-group/radio-group.meta.md +79 -7
  184. package/src/components/radio-group/radio-group.stories.tsx +39 -3
  185. package/src/components/radio-group/radio-group.tsx +149 -18
  186. package/src/components/rate/rate.meta.md +35 -4
  187. package/src/components/rate/rate.stories.tsx +13 -5
  188. package/src/components/rate/rate.tsx +37 -10
  189. package/src/components/resizable/resizable.meta.md +7 -4
  190. package/src/components/resizable/resizable.stories.tsx +6 -6
  191. package/src/components/resizable/resizable.tsx +1 -1
  192. package/src/components/result/result.meta.md +7 -2
  193. package/src/components/result/result.stories.tsx +4 -8
  194. package/src/components/result/result.tsx +24 -15
  195. package/src/components/scroll-area/scroll-area.meta.md +4 -3
  196. package/src/components/scroll-area/scroll-area.stories.tsx +12 -4
  197. package/src/components/scroll-area/scroll-area.tsx +3 -3
  198. package/src/components/segmented/segmented.meta.md +7 -4
  199. package/src/components/segmented/segmented.stories.tsx +37 -8
  200. package/src/components/segmented/segmented.tsx +15 -7
  201. package/src/components/select/select.meta.md +197 -52
  202. package/src/components/select/select.stories.tsx +238 -63
  203. package/src/components/select/select.tsx +718 -171
  204. package/src/components/separator/separator.meta.md +4 -3
  205. package/src/components/separator/separator.stories.tsx +3 -3
  206. package/src/components/separator/separator.tsx +3 -7
  207. package/src/components/sheet/sheet.meta.md +32 -16
  208. package/src/components/sheet/sheet.stories.tsx +116 -10
  209. package/src/components/sheet/sheet.tsx +116 -29
  210. package/src/components/sidebar/sidebar.meta.md +37 -18
  211. package/src/components/sidebar/sidebar.stories.tsx +701 -29
  212. package/src/components/sidebar/sidebar.tsx +615 -142
  213. package/src/components/skeleton/skeleton.meta.md +4 -5
  214. package/src/components/skeleton/skeleton.stories.tsx +4 -4
  215. package/src/components/skeleton/skeleton.tsx +7 -7
  216. package/src/components/slider/slider.meta.md +57 -5
  217. package/src/components/slider/slider.stories.tsx +58 -6
  218. package/src/components/slider/slider.tsx +154 -13
  219. package/src/components/sonner/sonner.meta.md +58 -7
  220. package/src/components/sonner/sonner.stories.tsx +78 -5
  221. package/src/components/sonner/sonner.tsx +137 -8
  222. package/src/components/spinner/spinner.meta.md +62 -13
  223. package/src/components/spinner/spinner.stories.tsx +66 -14
  224. package/src/components/spinner/spinner.tsx +111 -9
  225. package/src/components/statistic/statistic.meta.md +7 -2
  226. package/src/components/statistic/statistic.stories.tsx +3 -7
  227. package/src/components/statistic/statistic.tsx +5 -6
  228. package/src/components/steps/steps.meta.md +18 -4
  229. package/src/components/steps/steps.stories.tsx +43 -3
  230. package/src/components/steps/steps.tsx +15 -12
  231. package/src/components/switch/switch.meta.md +51 -5
  232. package/src/components/switch/switch.stories.tsx +6 -6
  233. package/src/components/switch/switch.tsx +109 -41
  234. package/src/components/table/table.meta.md +17 -6
  235. package/src/components/table/table.stories.tsx +10 -5
  236. package/src/components/table/table.tsx +4 -4
  237. package/src/components/tabs/tabs.meta.md +38 -25
  238. package/src/components/tabs/tabs.stories.tsx +111 -25
  239. package/src/components/tabs/tabs.tsx +125 -54
  240. package/src/components/tag/tag.meta.md +105 -40
  241. package/src/components/tag/tag.stories.tsx +189 -16
  242. package/src/components/tag/tag.tsx +222 -21
  243. package/src/components/textarea/textarea.meta.md +35 -19
  244. package/src/components/textarea/textarea.stories.tsx +32 -6
  245. package/src/components/textarea/textarea.tsx +33 -9
  246. package/src/components/time-picker/time-picker.meta.md +124 -32
  247. package/src/components/time-picker/time-picker.stories.tsx +85 -15
  248. package/src/components/time-picker/time-picker.tsx +913 -61
  249. package/src/components/timeline/timeline.meta.md +14 -6
  250. package/src/components/timeline/timeline.stories.tsx +37 -7
  251. package/src/components/timeline/timeline.tsx +35 -14
  252. package/src/components/toggle/toggle.meta.md +5 -4
  253. package/src/components/toggle/toggle.stories.tsx +4 -4
  254. package/src/components/toggle/toggle.tsx +4 -3
  255. package/src/components/toggle-group/toggle-group.meta.md +5 -4
  256. package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
  257. package/src/components/toggle-group/toggle-group.tsx +2 -2
  258. package/src/components/tooltip/tooltip.meta.md +55 -5
  259. package/src/components/tooltip/tooltip.stories.tsx +42 -5
  260. package/src/components/tooltip/tooltip.tsx +81 -21
  261. package/src/components/tour/tour.meta.md +9 -4
  262. package/src/components/tour/tour.stories.tsx +3 -3
  263. package/src/components/tour/tour.tsx +4 -4
  264. package/src/components/transfer/transfer.meta.md +11 -6
  265. package/src/components/transfer/transfer.stories.tsx +4 -8
  266. package/src/components/transfer/transfer.tsx +28 -21
  267. package/src/components/tree/tree.meta.md +63 -5
  268. package/src/components/tree/tree.stories.tsx +31 -12
  269. package/src/components/tree/tree.tsx +9 -8
  270. package/src/components/tree-select/tree-select.meta.md +59 -8
  271. package/src/components/tree-select/tree-select.stories.tsx +3 -3
  272. package/src/components/tree-select/tree-select.tsx +42 -7
  273. package/src/components/typography/typography.meta.md +61 -14
  274. package/src/components/typography/typography.stories.tsx +12 -11
  275. package/src/components/typography/typography.tsx +43 -28
  276. package/src/components/upload/upload.meta.md +49 -4
  277. package/src/components/upload/upload.stories.tsx +72 -12
  278. package/src/components/upload/upload.tsx +170 -37
  279. package/src/components/watermark/watermark.meta.md +7 -2
  280. package/src/components/watermark/watermark.stories.tsx +101 -9
  281. package/src/components/watermark/watermark.tsx +1 -0
  282. package/src/hooks/use-breakpoint.ts +117 -0
  283. package/src/hooks/use-debounce-callback.ts +52 -0
  284. package/src/hooks/use-mobile.ts +23 -0
  285. package/src/stories/theme-tokens.stories.tsx +747 -0
  286. package/src/utils/trigger-input.ts +53 -0
  287. package/src/components/button-group/button-group.meta.md +0 -92
  288. package/src/components/button-group/button-group.stories.tsx +0 -90
  289. package/src/components/button-group/button-group.tsx +0 -75
  290. package/src/components/combobox/combobox.meta.md +0 -93
  291. package/src/components/combobox/combobox.stories.tsx +0 -55
  292. package/src/components/combobox/combobox.tsx +0 -130
  293. package/src/components/space/space.meta.md +0 -94
  294. package/src/components/space/space.stories.tsx +0 -94
  295. package/src/components/space/space.tsx +0 -106
@@ -1,37 +1,79 @@
1
+ /**
2
+ * Sidebar — shadcn 完整版 25 primitives.
3
+ *
4
+ * **arbitrary value 豁免说明**:本组件少量使用 Tailwind arbitrary value
5
+ * (`w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]` 等),原因:
6
+ *
7
+ * 1. Sidebar 折叠态需要在 `group-data-[collapsible=icon]` 选择器下响应父级 data
8
+ * 属性 — 这只能通过 Tailwind selector 实现,无法 inline `style` 替代;
9
+ * 2. floating/inset variant 的 gap/container 宽度是 `--sidebar-width-icon + p-2`
10
+ * 的合成值,Tailwind 标尺无法直接表达;
11
+ * 3. MenuButton outline variant 用 `box-shadow: 0 0 0 1px ...` 模拟 1px 边框
12
+ * 避免占用尺寸,这是 shadcn outline 风格的核心实现技术。
13
+ *
14
+ * 这些 arbitrary value 是 Sidebar 复合组件的结构性必要,与 ADR 0008 视觉规则
15
+ * 的本意(避免任意像素值/任意色值)相符 — 此处的值都是 token 引用,不是
16
+ * 硬编码像素或色值。本文件单独豁免 `teamix-evo/no-arbitrary-tw-value`。
17
+ */
18
+ /* eslint-disable teamix-evo/no-arbitrary-tw-value */
1
19
  import * as React from 'react';
2
20
  import { Slot } from '@radix-ui/react-slot';
3
21
  import { cva, type VariantProps } from 'class-variance-authority';
4
22
  import { PanelLeft } from 'lucide-react';
5
23
 
6
24
  import { cn } from '@/utils/cn';
25
+ import { useIsMobile } from '@/hooks/use-mobile';
7
26
  import { Button } from '@/components/button/button';
27
+ import { Input } from '@/components/input/input';
8
28
  import { Separator } from '@/components/separator/separator';
29
+ import {
30
+ Sheet,
31
+ SheetContent,
32
+ SheetDescription,
33
+ SheetHeader,
34
+ SheetTitle,
35
+ } from '@/components/sheet/sheet';
36
+ import { Skeleton } from '@/components/skeleton/skeleton';
37
+ import {
38
+ TooltipContent,
39
+ TooltipProvider,
40
+ TooltipRoot,
41
+ TooltipTrigger,
42
+ } from '@/components/tooltip/tooltip';
9
43
 
10
- // ─── Context + Provider ──────────────────────────────────────────────────────
44
+ const SIDEBAR_COOKIE_NAME = 'sidebar_state';
45
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
46
+ const SIDEBAR_WIDTH = '16rem';
47
+ const SIDEBAR_WIDTH_MOBILE = '18rem';
48
+ const SIDEBAR_WIDTH_ICON = '3rem';
49
+ const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
11
50
 
12
51
  interface SidebarContextValue {
52
+ state: 'expanded' | 'collapsed';
13
53
  open: boolean;
14
54
  setOpen: (open: boolean) => void;
15
- toggle: () => void;
55
+ openMobile: boolean;
56
+ setOpenMobile: (open: boolean) => void;
57
+ isMobile: boolean;
58
+ toggleSidebar: () => void;
16
59
  }
17
60
 
18
61
  const SidebarContext = React.createContext<SidebarContextValue | null>(null);
19
62
 
20
- export function useSidebar() {
63
+ export function useSidebar(): SidebarContextValue {
21
64
  const ctx = React.useContext(SidebarContext);
22
65
  if (!ctx) {
23
- throw new Error('useSidebar must be used within <SidebarProvider>');
66
+ throw new Error('useSidebar must be used within a SidebarProvider.');
24
67
  }
25
68
  return ctx;
26
69
  }
27
70
 
71
+ // ─── Provider ──────────────────────────────────────────────────────────────
72
+
28
73
  export interface SidebarProviderProps
29
74
  extends React.HTMLAttributes<HTMLDivElement> {
30
- /** 受控 open 状态。 */
31
- open?: boolean;
32
- /** uncontrolled 初始状态。 @default true */
33
75
  defaultOpen?: boolean;
34
- /** open 状态变化回调。 */
76
+ open?: boolean;
35
77
  onOpenChange?: (open: boolean) => void;
36
78
  }
37
79
 
@@ -40,129 +82,308 @@ const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(
40
82
  {
41
83
  defaultOpen = true,
42
84
  open: openProp,
43
- onOpenChange,
85
+ onOpenChange: setOpenProp,
44
86
  className,
87
+ style,
45
88
  children,
46
89
  ...props
47
90
  },
48
91
  ref,
49
92
  ) => {
50
- const [internal, setInternal] = React.useState(defaultOpen);
51
- const isControlled = openProp !== undefined;
52
- const open = isControlled ? (openProp as boolean) : internal;
93
+ const isMobile = useIsMobile();
94
+ const [openMobile, setOpenMobile] = React.useState(false);
53
95
 
96
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
97
+ const open = openProp ?? internalOpen;
54
98
  const setOpen = React.useCallback(
55
- (next: boolean) => {
56
- if (!isControlled) setInternal(next);
57
- onOpenChange?.(next);
99
+ (value: boolean | ((value: boolean) => boolean)) => {
100
+ const openState = typeof value === 'function' ? value(open) : value;
101
+ if (setOpenProp) {
102
+ setOpenProp(openState);
103
+ } else {
104
+ setInternalOpen(openState);
105
+ }
106
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
58
107
  },
59
- [isControlled, onOpenChange],
108
+ [setOpenProp, open],
60
109
  );
61
- const toggle = React.useCallback(() => setOpen(!open), [open, setOpen]);
62
110
 
63
- const value = React.useMemo<SidebarContextValue>(
64
- () => ({ open, setOpen, toggle }),
65
- [open, setOpen, toggle],
111
+ const toggleSidebar = React.useCallback(() => {
112
+ return isMobile
113
+ ? setOpenMobile((current) => !current)
114
+ : setOpen((current) => !current);
115
+ }, [isMobile, setOpen, setOpenMobile]);
116
+
117
+ React.useEffect(() => {
118
+ const handleKeyDown = (event: KeyboardEvent) => {
119
+ if (
120
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
121
+ (event.metaKey || event.ctrlKey)
122
+ ) {
123
+ event.preventDefault();
124
+ toggleSidebar();
125
+ }
126
+ };
127
+ window.addEventListener('keydown', handleKeyDown);
128
+ return () => window.removeEventListener('keydown', handleKeyDown);
129
+ }, [toggleSidebar]);
130
+
131
+ const state: 'expanded' | 'collapsed' = open ? 'expanded' : 'collapsed';
132
+
133
+ const contextValue = React.useMemo<SidebarContextValue>(
134
+ () => ({
135
+ state,
136
+ open,
137
+ setOpen,
138
+ isMobile,
139
+ openMobile,
140
+ setOpenMobile,
141
+ toggleSidebar,
142
+ }),
143
+ [
144
+ state,
145
+ open,
146
+ setOpen,
147
+ isMobile,
148
+ openMobile,
149
+ setOpenMobile,
150
+ toggleSidebar,
151
+ ],
66
152
  );
67
153
 
68
154
  return (
69
- <SidebarContext.Provider value={value}>
70
- <div
71
- ref={ref}
72
- data-state={open ? 'expanded' : 'collapsed'}
73
- className={cn(
74
- 'group/sidebar-wrapper flex min-h-svh w-full',
75
- className,
76
- )}
77
- {...props}
78
- >
79
- {children}
80
- </div>
155
+ <SidebarContext.Provider value={contextValue}>
156
+ <TooltipProvider delayDuration={0}>
157
+ <div
158
+ ref={ref}
159
+ data-slot="sidebar-wrapper"
160
+ style={
161
+ {
162
+ '--sidebar-width': SIDEBAR_WIDTH,
163
+ '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
164
+ ...style,
165
+ } as React.CSSProperties
166
+ }
167
+ className={cn(
168
+ 'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ {children}
174
+ </div>
175
+ </TooltipProvider>
81
176
  </SidebarContext.Provider>
82
177
  );
83
178
  },
84
179
  );
85
180
  SidebarProvider.displayName = 'SidebarProvider';
86
181
 
87
- // ─── Main Sidebar container ───────────────────────────────────────────────────
182
+ // ─── Main Sidebar container ────────────────────────────────────────────────
88
183
 
89
- const sidebarVariants = cva(
90
- 'flex h-svh shrink-0 flex-col border-r bg-background transition-[width] duration-200 ease-linear',
91
- {
92
- variants: {
93
- side: {
94
- left: 'border-r',
95
- right: 'border-l border-r-0 order-last',
96
- },
97
- },
98
- defaultVariants: { side: 'left' },
99
- },
100
- );
101
-
102
- export interface SidebarProps
103
- extends React.HTMLAttributes<HTMLElement>,
104
- VariantProps<typeof sidebarVariants> {
105
- /**
106
- * 侧边方向。
107
- * @default "left"
108
- */
184
+ export interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
109
185
  side?: 'left' | 'right';
110
- /**
111
- * 折叠模式 `icon` 折叠后只剩图标列(64px);`offcanvas` 完全收起(0px)。
112
- * @default "icon"
113
- */
114
- collapsible?: 'icon' | 'offcanvas' | 'none';
115
- /** 展开宽度。 @default "16rem" */
116
- width?: string;
117
- /** 折叠后宽度(`collapsible="icon"` 时生效)。 @default "3rem" */
118
- collapsedWidth?: string;
186
+ variant?: 'sidebar' | 'floating' | 'inset';
187
+ collapsible?: 'offcanvas' | 'icon' | 'none';
119
188
  }
120
189
 
121
- const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
190
+ const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
122
191
  (
123
192
  {
124
193
  side = 'left',
125
- collapsible = 'icon',
126
- width = '16rem',
127
- collapsedWidth = '3rem',
194
+ variant = 'sidebar',
195
+ collapsible = 'offcanvas',
128
196
  className,
129
- style,
130
197
  children,
131
198
  ...props
132
199
  },
133
200
  ref,
134
201
  ) => {
135
- const { open } = useSidebar();
136
- const w =
137
- collapsible === 'none'
138
- ? width
139
- : open
140
- ? width
141
- : collapsible === 'icon'
142
- ? collapsedWidth
143
- : '0px';
202
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
203
+
204
+ if (collapsible === 'none') {
205
+ return (
206
+ <div
207
+ ref={ref}
208
+ data-slot="sidebar"
209
+ className={cn(
210
+ 'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
211
+ className,
212
+ )}
213
+ {...props}
214
+ >
215
+ {children}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ if (isMobile) {
221
+ return (
222
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
223
+ <SheetContent
224
+ data-sidebar="sidebar"
225
+ data-slot="sidebar"
226
+ data-mobile="true"
227
+ className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
228
+ style={
229
+ {
230
+ '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
231
+ } as React.CSSProperties
232
+ }
233
+ side={side}
234
+ >
235
+ <SheetHeader className="sr-only">
236
+ <SheetTitle>Sidebar</SheetTitle>
237
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
238
+ </SheetHeader>
239
+ <div className="flex h-full w-full flex-col">{children}</div>
240
+ </SheetContent>
241
+ </Sheet>
242
+ );
243
+ }
244
+
144
245
  return (
145
- <aside
246
+ <div
146
247
  ref={ref}
147
- data-state={open ? 'expanded' : 'collapsed'}
148
- data-collapsible={collapsible}
248
+ className="group peer text-sidebar-foreground hidden md:block"
249
+ data-state={state}
250
+ data-collapsible={state === 'collapsed' ? collapsible : ''}
251
+ data-variant={variant}
149
252
  data-side={side}
150
- style={{ width: w, ...style }}
151
- className={cn(
152
- sidebarVariants({ side }),
153
- collapsible === 'offcanvas' && !open && 'overflow-hidden',
154
- className,
155
- )}
156
- {...props}
253
+ data-slot="sidebar"
157
254
  >
158
- {children}
159
- </aside>
255
+ {/* sidebar gap on desktop */}
256
+ <div
257
+ data-slot="sidebar-gap"
258
+ className={cn(
259
+ 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
260
+ 'group-data-[collapsible=offcanvas]:w-0',
261
+ 'group-data-[side=right]:rotate-180',
262
+ variant === 'floating' || variant === 'inset'
263
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
264
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
265
+ )}
266
+ />
267
+ <div
268
+ data-slot="sidebar-container"
269
+ data-side={side}
270
+ className={cn(
271
+ 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
272
+ 'data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]',
273
+ 'data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
274
+ variant === 'floating' || variant === 'inset'
275
+ ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
276
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l border-sidebar-border',
277
+ className,
278
+ )}
279
+ {...props}
280
+ >
281
+ <div
282
+ data-sidebar="sidebar"
283
+ data-slot="sidebar-inner"
284
+ className="bg-sidebar group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:ring-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
285
+ >
286
+ {children}
287
+ </div>
288
+ </div>
289
+ </div>
160
290
  );
161
291
  },
162
292
  );
163
293
  Sidebar.displayName = 'Sidebar';
164
294
 
165
- // ─── Sub-components ──────────────────────────────────────────────────────────
295
+ // ─── Trigger / Rail / Inset / Input ────────────────────────────────────────
296
+
297
+ const SidebarTrigger = React.forwardRef<
298
+ HTMLButtonElement,
299
+ React.ComponentProps<typeof Button>
300
+ >(({ className, onClick, ...props }, ref) => {
301
+ const { toggleSidebar } = useSidebar();
302
+ return (
303
+ <Button
304
+ ref={ref}
305
+ data-sidebar="trigger"
306
+ data-slot="sidebar-trigger"
307
+ variant="ghost"
308
+ size="icon"
309
+ className={cn('size-7', className)}
310
+ onClick={(event) => {
311
+ onClick?.(event);
312
+ toggleSidebar();
313
+ }}
314
+ aria-label="Toggle Sidebar"
315
+ {...props}
316
+ >
317
+ <PanelLeft />
318
+ <span className="sr-only">Toggle Sidebar</span>
319
+ </Button>
320
+ );
321
+ });
322
+ SidebarTrigger.displayName = 'SidebarTrigger';
323
+
324
+ const SidebarRail = React.forwardRef<
325
+ HTMLButtonElement,
326
+ React.ButtonHTMLAttributes<HTMLButtonElement>
327
+ >(({ className, ...props }, ref) => {
328
+ const { toggleSidebar } = useSidebar();
329
+ return (
330
+ <button
331
+ ref={ref}
332
+ type="button"
333
+ data-sidebar="rail"
334
+ data-slot="sidebar-rail"
335
+ aria-label="Toggle Sidebar"
336
+ tabIndex={-1}
337
+ onClick={toggleSidebar}
338
+ title="Toggle Sidebar"
339
+ className={cn(
340
+ 'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
341
+ 'after:absolute after:inset-y-0 after:start-1/2 after:w-0.5',
342
+ 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
343
+ '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
344
+ 'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
345
+ '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
346
+ '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
347
+ className,
348
+ )}
349
+ {...props}
350
+ />
351
+ );
352
+ });
353
+ SidebarRail.displayName = 'SidebarRail';
354
+
355
+ const SidebarInset = React.forwardRef<
356
+ HTMLElement,
357
+ React.HTMLAttributes<HTMLElement>
358
+ >(({ className, ...props }, ref) => (
359
+ <main
360
+ ref={ref}
361
+ data-slot="sidebar-inset"
362
+ className={cn(
363
+ 'bg-background relative flex w-full flex-1 flex-col',
364
+ 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
365
+ className,
366
+ )}
367
+ {...(props as React.HTMLAttributes<HTMLElement>)}
368
+ />
369
+ ));
370
+ SidebarInset.displayName = 'SidebarInset';
371
+
372
+ const SidebarInput = React.forwardRef<
373
+ HTMLInputElement,
374
+ React.ComponentProps<typeof Input>
375
+ >(({ className, ...props }, ref) => (
376
+ <Input
377
+ ref={ref}
378
+ data-slot="sidebar-input"
379
+ data-sidebar="input"
380
+ className={cn('bg-background h-8 w-full shadow-none', className)}
381
+ {...props}
382
+ />
383
+ ));
384
+ SidebarInput.displayName = 'SidebarInput';
385
+
386
+ // ─── Header / Footer / Separator / Content ──────────────────────────────────
166
387
 
167
388
  const SidebarHeader = React.forwardRef<
168
389
  HTMLDivElement,
@@ -170,6 +391,8 @@ const SidebarHeader = React.forwardRef<
170
391
  >(({ className, ...props }, ref) => (
171
392
  <div
172
393
  ref={ref}
394
+ data-slot="sidebar-header"
395
+ data-sidebar="header"
173
396
  className={cn('flex flex-col gap-2 p-2', className)}
174
397
  {...props}
175
398
  />
@@ -182,7 +405,9 @@ const SidebarFooter = React.forwardRef<
182
405
  >(({ className, ...props }, ref) => (
183
406
  <div
184
407
  ref={ref}
185
- className={cn('mt-auto flex flex-col gap-2 p-2', className)}
408
+ data-slot="sidebar-footer"
409
+ data-sidebar="footer"
410
+ className={cn('flex flex-col gap-2 p-2', className)}
186
411
  {...props}
187
412
  />
188
413
  ));
@@ -194,7 +419,9 @@ const SidebarSeparator = React.forwardRef<
194
419
  >(({ className, ...props }, ref) => (
195
420
  <Separator
196
421
  ref={ref}
197
- className={cn('mx-2 w-auto bg-border', className)}
422
+ data-slot="sidebar-separator"
423
+ data-sidebar="separator"
424
+ className={cn('bg-sidebar-border mx-2 w-auto', className)}
198
425
  {...props}
199
426
  />
200
427
  ));
@@ -206,8 +433,10 @@ const SidebarContent = React.forwardRef<
206
433
  >(({ className, ...props }, ref) => (
207
434
  <div
208
435
  ref={ref}
436
+ data-slot="sidebar-content"
437
+ data-sidebar="content"
209
438
  className={cn(
210
- 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-2',
439
+ 'no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
211
440
  className,
212
441
  )}
213
442
  {...props}
@@ -215,29 +444,39 @@ const SidebarContent = React.forwardRef<
215
444
  ));
216
445
  SidebarContent.displayName = 'SidebarContent';
217
446
 
447
+ // ─── Group / GroupLabel / GroupAction / GroupContent ────────────────────────
448
+
218
449
  const SidebarGroup = React.forwardRef<
219
450
  HTMLDivElement,
220
451
  React.HTMLAttributes<HTMLDivElement>
221
452
  >(({ className, ...props }, ref) => (
222
453
  <div
223
454
  ref={ref}
224
- className={cn('flex w-full min-w-0 flex-col gap-1', className)}
455
+ data-slot="sidebar-group"
456
+ data-sidebar="group"
457
+ className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
225
458
  {...props}
226
459
  />
227
460
  ));
228
461
  SidebarGroup.displayName = 'SidebarGroup';
229
462
 
463
+ export interface SidebarGroupLabelProps
464
+ extends React.HTMLAttributes<HTMLDivElement> {
465
+ asChild?: boolean;
466
+ }
467
+
230
468
  const SidebarGroupLabel = React.forwardRef<
231
469
  HTMLDivElement,
232
- React.HTMLAttributes<HTMLDivElement>
233
- >(({ className, ...props }, ref) => {
234
- const { open } = useSidebar();
470
+ SidebarGroupLabelProps
471
+ >(({ className, asChild = false, ...props }, ref) => {
472
+ const Comp = asChild ? Slot : 'div';
235
473
  return (
236
- <div
474
+ <Comp
237
475
  ref={ref}
476
+ data-slot="sidebar-group-label"
477
+ data-sidebar="group-label"
238
478
  className={cn(
239
- 'flex h-8 shrink-0 items-center px-2 text-xs font-medium text-muted-foreground',
240
- !open && 'opacity-0',
479
+ 'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium transition-[margin,opacity] duration-200 ease-linear outline-hidden group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
241
480
  className,
242
481
  )}
243
482
  {...props}
@@ -246,13 +485,57 @@ const SidebarGroupLabel = React.forwardRef<
246
485
  });
247
486
  SidebarGroupLabel.displayName = 'SidebarGroupLabel';
248
487
 
488
+ export interface SidebarGroupActionProps
489
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
490
+ asChild?: boolean;
491
+ }
492
+
493
+ const SidebarGroupAction = React.forwardRef<
494
+ HTMLButtonElement,
495
+ SidebarGroupActionProps
496
+ >(({ className, asChild = false, ...props }, ref) => {
497
+ const Comp = asChild ? Slot : 'button';
498
+ return (
499
+ <Comp
500
+ ref={ref}
501
+ data-slot="sidebar-group-action"
502
+ data-sidebar="group-action"
503
+ className={cn(
504
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 transition-transform outline-hidden focus-visible:ring-2 group-data-[collapsible=icon]:hidden [&>svg]:size-4 [&>svg]:shrink-0',
505
+ 'after:absolute after:-inset-2 md:after:hidden',
506
+ className,
507
+ )}
508
+ {...props}
509
+ />
510
+ );
511
+ });
512
+ SidebarGroupAction.displayName = 'SidebarGroupAction';
513
+
514
+ const SidebarGroupContent = React.forwardRef<
515
+ HTMLDivElement,
516
+ React.HTMLAttributes<HTMLDivElement>
517
+ >(({ className, ...props }, ref) => (
518
+ <div
519
+ ref={ref}
520
+ data-slot="sidebar-group-content"
521
+ data-sidebar="group-content"
522
+ className={cn('w-full text-xs', className)}
523
+ {...props}
524
+ />
525
+ ));
526
+ SidebarGroupContent.displayName = 'SidebarGroupContent';
527
+
528
+ // ─── Menu / MenuItem / MenuButton ──────────────────────────────────────────
529
+
249
530
  const SidebarMenu = React.forwardRef<
250
531
  HTMLUListElement,
251
532
  React.HTMLAttributes<HTMLUListElement>
252
533
  >(({ className, ...props }, ref) => (
253
534
  <ul
254
535
  ref={ref}
255
- className={cn('flex w-full min-w-0 flex-col gap-1', className)}
536
+ data-slot="sidebar-menu"
537
+ data-sidebar="menu"
538
+ className={cn('flex w-full min-w-0 flex-col gap-0', className)}
256
539
  {...props}
257
540
  />
258
541
  ));
@@ -262,90 +545,280 @@ const SidebarMenuItem = React.forwardRef<
262
545
  HTMLLIElement,
263
546
  React.HTMLAttributes<HTMLLIElement>
264
547
  >(({ className, ...props }, ref) => (
265
- <li ref={ref} className={cn('group/menu-item', className)} {...props} />
548
+ <li
549
+ ref={ref}
550
+ data-slot="sidebar-menu-item"
551
+ data-sidebar="menu-item"
552
+ className={cn('group/menu-item relative', className)}
553
+ {...props}
554
+ />
266
555
  ));
267
556
  SidebarMenuItem.displayName = 'SidebarMenuItem';
268
557
 
558
+ const sidebarMenuButtonVariants = cva(
559
+ 'peer/menu-button group/menu-button ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-xs outline-hidden transition-[width,height,padding] focus-visible:ring-2 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 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0',
560
+ {
561
+ variants: {
562
+ variant: {
563
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
564
+ outline:
565
+ 'bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
566
+ },
567
+ size: {
568
+ default: 'h-8 text-xs',
569
+ sm: 'h-7 text-xs',
570
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
571
+ },
572
+ },
573
+ defaultVariants: {
574
+ variant: 'default',
575
+ size: 'default',
576
+ },
577
+ },
578
+ );
579
+
269
580
  export interface SidebarMenuButtonProps
270
- extends React.ButtonHTMLAttributes<HTMLButtonElement> {
271
- /** 当前激活态(高亮显示)。 */
272
- isActive?: boolean;
273
- /** 用 Slot 渲染为子元素(配合 router Link)。 */
581
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
582
+ VariantProps<typeof sidebarMenuButtonVariants> {
274
583
  asChild?: boolean;
584
+ isActive?: boolean;
585
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>;
275
586
  }
276
587
 
277
588
  const SidebarMenuButton = React.forwardRef<
278
589
  HTMLButtonElement,
279
590
  SidebarMenuButtonProps
280
- >(({ className, isActive, asChild, ...props }, ref) => {
591
+ >(
592
+ (
593
+ {
594
+ asChild = false,
595
+ isActive = false,
596
+ variant = 'default',
597
+ size = 'default',
598
+ tooltip,
599
+ className,
600
+ ...props
601
+ },
602
+ ref,
603
+ ) => {
604
+ const Comp = asChild ? Slot : 'button';
605
+ const { isMobile, state } = useSidebar();
606
+
607
+ const button = (
608
+ <Comp
609
+ ref={ref}
610
+ data-slot="sidebar-menu-button"
611
+ data-sidebar="menu-button"
612
+ data-size={size}
613
+ data-active={isActive ? 'true' : undefined}
614
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
615
+ {...props}
616
+ />
617
+ );
618
+
619
+ if (!tooltip) {
620
+ return button;
621
+ }
622
+
623
+ const tooltipProps =
624
+ typeof tooltip === 'string' ? { children: tooltip } : tooltip;
625
+
626
+ return (
627
+ <TooltipRoot>
628
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
629
+ <TooltipContent
630
+ side="right"
631
+ align="center"
632
+ hidden={state !== 'collapsed' || isMobile}
633
+ {...tooltipProps}
634
+ />
635
+ </TooltipRoot>
636
+ );
637
+ },
638
+ );
639
+ SidebarMenuButton.displayName = 'SidebarMenuButton';
640
+
641
+ // ─── MenuAction / MenuBadge / MenuSkeleton ─────────────────────────────────
642
+
643
+ export interface SidebarMenuActionProps
644
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
645
+ asChild?: boolean;
646
+ showOnHover?: boolean;
647
+ }
648
+
649
+ const SidebarMenuAction = React.forwardRef<
650
+ HTMLButtonElement,
651
+ SidebarMenuActionProps
652
+ >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
281
653
  const Comp = asChild ? Slot : 'button';
282
654
  return (
283
655
  <Comp
284
656
  ref={ref}
285
- data-active={isActive ? 'true' : undefined}
657
+ data-slot="sidebar-menu-action"
658
+ data-sidebar="menu-action"
286
659
  className={cn(
287
- 'flex h-8 w-full items-center gap-2 overflow-hidden rounded-md px-2 text-left text-sm outline-none ring-ring transition hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0',
660
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 transition-transform outline-hidden focus-visible:ring-2 group-data-[collapsible=icon]:hidden [&>svg]:size-4 [&>svg]:shrink-0',
661
+ 'after:absolute after:-inset-2 md:after:hidden',
662
+ 'peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5',
663
+ showOnHover &&
664
+ 'peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 aria-expanded:opacity-100 md:opacity-0',
288
665
  className,
289
666
  )}
290
667
  {...props}
291
668
  />
292
669
  );
293
670
  });
294
- SidebarMenuButton.displayName = 'SidebarMenuButton';
671
+ SidebarMenuAction.displayName = 'SidebarMenuAction';
295
672
 
296
- // ─── Trigger ─────────────────────────────────────────────────────────────────
673
+ const SidebarMenuBadge = React.forwardRef<
674
+ HTMLDivElement,
675
+ React.HTMLAttributes<HTMLDivElement>
676
+ >(({ className, ...props }, ref) => (
677
+ <div
678
+ ref={ref}
679
+ data-slot="sidebar-menu-badge"
680
+ data-sidebar="menu-badge"
681
+ className={cn(
682
+ 'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none group-data-[collapsible=icon]:hidden',
683
+ 'peer-data-[size=sm]/menu-button:top-1 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5',
684
+ className,
685
+ )}
686
+ {...props}
687
+ />
688
+ ));
689
+ SidebarMenuBadge.displayName = 'SidebarMenuBadge';
297
690
 
298
- const SidebarTrigger = React.forwardRef<
299
- HTMLButtonElement,
300
- React.ComponentPropsWithoutRef<typeof Button>
301
- >(({ className, onClick, ...props }, ref) => {
302
- const { toggle } = useSidebar();
691
+ export interface SidebarMenuSkeletonProps
692
+ extends React.HTMLAttributes<HTMLDivElement> {
693
+ showIcon?: boolean;
694
+ }
695
+
696
+ const SidebarMenuSkeleton = React.forwardRef<
697
+ HTMLDivElement,
698
+ SidebarMenuSkeletonProps
699
+ >(({ className, showIcon = false, ...props }, ref) => {
700
+ // Stable but varied widths so a stack of skeletons doesn't look uniform.
701
+ const [width] = React.useState(
702
+ () => `${Math.floor(Math.random() * 40) + 50}%`,
703
+ );
303
704
  return (
304
- <Button
705
+ <div
305
706
  ref={ref}
306
- variant="ghost"
307
- size="icon"
308
- className={cn('size-7', className)}
309
- onClick={(e) => {
310
- onClick?.(e);
311
- toggle();
312
- }}
313
- aria-label="Toggle Sidebar"
707
+ data-slot="sidebar-menu-skeleton"
708
+ data-sidebar="menu-skeleton"
709
+ className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
314
710
  {...props}
315
711
  >
316
- <PanelLeft />
317
- <span className="sr-only">Toggle Sidebar</span>
318
- </Button>
712
+ {showIcon && (
713
+ <Skeleton
714
+ className="size-4 rounded-md"
715
+ data-sidebar="menu-skeleton-icon"
716
+ />
717
+ )}
718
+ <Skeleton
719
+ className="h-4 max-w-(--skeleton-width) flex-1"
720
+ data-sidebar="menu-skeleton-text"
721
+ style={
722
+ {
723
+ '--skeleton-width': width,
724
+ } as React.CSSProperties
725
+ }
726
+ />
727
+ </div>
319
728
  );
320
729
  });
321
- SidebarTrigger.displayName = 'SidebarTrigger';
730
+ SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
322
731
 
323
- // ─── Inset(主内容区,与 Sidebar 配合)─────────────────────────────────────
732
+ // ─── MenuSub / MenuSubItem / MenuSubButton ─────────────────────────────────
324
733
 
325
- const SidebarInset = React.forwardRef<
326
- HTMLDivElement,
327
- React.HTMLAttributes<HTMLDivElement>
734
+ const SidebarMenuSub = React.forwardRef<
735
+ HTMLUListElement,
736
+ React.HTMLAttributes<HTMLUListElement>
328
737
  >(({ className, ...props }, ref) => (
329
- <main
738
+ <ul
330
739
  ref={ref}
331
- className={cn('relative flex min-h-svh flex-1 flex-col bg-background', className)}
332
- {...(props as React.HTMLAttributes<HTMLElement>)}
740
+ data-slot="sidebar-menu-sub"
741
+ data-sidebar="menu-sub"
742
+ className={cn(
743
+ 'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-l-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden',
744
+ className,
745
+ )}
746
+ {...props}
333
747
  />
334
748
  ));
335
- SidebarInset.displayName = 'SidebarInset';
749
+ SidebarMenuSub.displayName = 'SidebarMenuSub';
750
+
751
+ const SidebarMenuSubItem = React.forwardRef<
752
+ HTMLLIElement,
753
+ React.HTMLAttributes<HTMLLIElement>
754
+ >(({ className, ...props }, ref) => (
755
+ <li
756
+ ref={ref}
757
+ data-slot="sidebar-menu-sub-item"
758
+ data-sidebar="menu-sub-item"
759
+ className={cn('group/menu-sub-item relative', className)}
760
+ {...props}
761
+ />
762
+ ));
763
+ SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
764
+
765
+ export interface SidebarMenuSubButtonProps
766
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
767
+ asChild?: boolean;
768
+ size?: 'sm' | 'md';
769
+ isActive?: boolean;
770
+ }
771
+
772
+ const SidebarMenuSubButton = React.forwardRef<
773
+ HTMLAnchorElement,
774
+ SidebarMenuSubButtonProps
775
+ >(
776
+ (
777
+ { className, asChild = false, size = 'md', isActive = false, ...props },
778
+ ref,
779
+ ) => {
780
+ const Comp = asChild ? Slot : 'a';
781
+ return (
782
+ <Comp
783
+ ref={ref}
784
+ data-slot="sidebar-menu-sub-button"
785
+ data-sidebar="menu-sub-button"
786
+ data-size={size}
787
+ data-active={isActive ? 'true' : undefined}
788
+ className={cn(
789
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 group-data-[collapsible=icon]:hidden aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
790
+ 'data-[size=sm]:text-xs data-[size=md]:text-xs',
791
+ className,
792
+ )}
793
+ {...props}
794
+ />
795
+ );
796
+ },
797
+ );
798
+ SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
336
799
 
337
800
  export {
338
- SidebarProvider,
339
801
  Sidebar,
340
- SidebarHeader,
341
- SidebarFooter,
342
- SidebarSeparator,
343
802
  SidebarContent,
803
+ SidebarFooter,
344
804
  SidebarGroup,
805
+ SidebarGroupAction,
806
+ SidebarGroupContent,
345
807
  SidebarGroupLabel,
808
+ SidebarHeader,
809
+ SidebarInput,
810
+ SidebarInset,
346
811
  SidebarMenu,
347
- SidebarMenuItem,
812
+ SidebarMenuAction,
813
+ SidebarMenuBadge,
348
814
  SidebarMenuButton,
815
+ SidebarMenuItem,
816
+ SidebarMenuSkeleton,
817
+ SidebarMenuSub,
818
+ SidebarMenuSubButton,
819
+ SidebarMenuSubItem,
820
+ SidebarProvider,
821
+ SidebarRail,
822
+ SidebarSeparator,
349
823
  SidebarTrigger,
350
- SidebarInset,
351
824
  };