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