@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.
- package/README.md +184 -184
- package/manifest.json +695 -487
- package/package.json +16 -10
- package/src/components/accordion/accordion.meta.md +5 -9
- package/src/components/accordion/accordion.stories.tsx +3 -3
- package/src/components/accordion/accordion.tsx +104 -8
- package/src/components/affix/affix.meta.md +21 -12
- package/src/components/affix/affix.stories.tsx +101 -26
- package/src/components/affix/affix.tsx +79 -9
- package/src/components/alert/alert.meta.md +52 -26
- package/src/components/alert/alert.stories.tsx +66 -21
- package/src/components/alert/alert.tsx +81 -34
- package/src/components/alert-dialog/alert-dialog.meta.md +48 -16
- package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
- package/src/components/alert-dialog/alert-dialog.tsx +60 -13
- package/src/components/anchor/anchor.meta.md +10 -14
- package/src/components/anchor/anchor.stories.tsx +3 -3
- package/src/components/anchor/anchor.tsx +2 -2
- package/src/components/app/app.meta.md +10 -14
- package/src/components/app/app.stories.tsx +6 -6
- package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -8
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
- package/src/components/auto-complete/auto-complete.meta.md +19 -20
- package/src/components/auto-complete/auto-complete.stories.tsx +44 -3
- package/src/components/auto-complete/auto-complete.tsx +119 -71
- package/src/components/avatar/avatar.meta.md +9 -22
- package/src/components/avatar/avatar.stories.tsx +21 -3
- package/src/components/avatar/avatar.tsx +24 -23
- package/src/components/badge/badge.meta.md +14 -18
- package/src/components/badge/badge.stories.tsx +2 -2
- package/src/components/badge/badge.tsx +2 -2
- package/src/components/breadcrumb/breadcrumb.meta.md +29 -20
- package/src/components/breadcrumb/breadcrumb.stories.tsx +120 -5
- package/src/components/breadcrumb/breadcrumb.tsx +22 -8
- package/src/components/button/button.meta.md +261 -29
- package/src/components/button/button.stories.tsx +549 -41
- package/src/components/button/button.tsx +341 -35
- package/src/components/calendar/calendar.meta.md +19 -14
- package/src/components/calendar/calendar.stories.tsx +5 -5
- package/src/components/calendar/calendar.tsx +73 -8
- package/src/components/card/card.meta.md +31 -34
- package/src/components/card/card.stories.tsx +34 -3
- package/src/components/card/card.tsx +147 -63
- package/src/components/carousel/carousel.meta.md +10 -14
- package/src/components/carousel/carousel.stories.tsx +1 -1
- package/src/components/cascader/cascader.meta.md +43 -22
- package/src/components/cascader/cascader.stories.tsx +13 -2
- package/src/components/cascader/cascader.tsx +439 -89
- package/src/components/checkbox/checkbox.meta.md +74 -24
- package/src/components/checkbox/checkbox.stories.tsx +160 -2
- package/src/components/checkbox/checkbox.tsx +79 -9
- package/src/components/collapsible/collapsible.meta.md +7 -6
- package/src/components/collapsible/collapsible.stories.tsx +2 -2
- package/src/components/collapsible/collapsible.tsx +93 -6
- package/src/components/color-picker/color-picker.meta.md +16 -20
- package/src/components/color-picker/color-picker.stories.tsx +86 -7
- package/src/components/color-picker/color-picker.tsx +19 -9
- package/src/components/command/command.meta.md +7 -11
- package/src/components/command/command.stories.tsx +4 -4
- package/src/components/command/command.tsx +18 -7
- package/src/components/context-menu/context-menu.meta.md +5 -25
- package/src/components/context-menu/context-menu.stories.tsx +4 -4
- package/src/components/context-menu/context-menu.tsx +21 -8
- package/src/components/data-table/data-table.meta.md +14 -18
- package/src/components/data-table/data-table.stories.tsx +1 -1
- package/src/components/data-table/data-table.tsx +2 -2
- package/src/components/date-picker/date-picker.meta.md +90 -41
- package/src/components/date-picker/date-picker.stories.tsx +55 -5
- package/src/components/date-picker/date-picker.tsx +1489 -91
- package/src/components/descriptions/descriptions.meta.md +12 -16
- package/src/components/descriptions/descriptions.stories.tsx +2 -2
- package/src/components/descriptions/descriptions.tsx +22 -14
- package/src/components/dialog/dialog.meta.md +67 -17
- package/src/components/dialog/dialog.stories.tsx +182 -20
- package/src/components/dialog/dialog.tsx +67 -15
- package/src/components/dialog/imperative.tsx +252 -0
- package/src/components/drawer/drawer.meta.md +27 -39
- package/src/components/drawer/drawer.stories.tsx +29 -12
- package/src/components/drawer/drawer.tsx +22 -114
- package/src/components/dropdown-menu/dropdown-menu.meta.md +64 -24
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +81 -3
- package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
- package/src/components/ellipsis/ellipsis.meta.md +87 -0
- package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
- package/src/components/ellipsis/ellipsis.tsx +153 -0
- package/src/components/empty/empty.meta.md +10 -14
- package/src/components/empty/empty.stories.tsx +3 -3
- package/src/components/empty/empty.tsx +10 -3
- package/src/components/field/field.meta.md +46 -25
- package/src/components/field/field.stories.tsx +380 -3
- package/src/components/field/field.tsx +263 -35
- package/src/components/filter-bar/filter-bar.meta.md +92 -0
- package/src/components/filter-bar/filter-bar.stories.tsx +1086 -0
- package/src/components/filter-bar/filter-bar.tsx +568 -0
- package/src/components/flex/flex.meta.md +59 -20
- package/src/components/flex/flex.stories.tsx +65 -10
- package/src/components/flex/flex.tsx +27 -4
- package/src/components/float-button/float-button.meta.md +10 -29
- package/src/components/float-button/float-button.stories.tsx +6 -6
- package/src/components/form/form.meta.md +31 -52
- package/src/components/form/form.stories.tsx +354 -4
- package/src/components/form/form.tsx +101 -35
- package/src/components/grid/grid.meta.md +4 -24
- package/src/components/grid/grid.stories.tsx +2 -2
- package/src/components/hover-card/hover-card.meta.md +9 -10
- package/src/components/hover-card/hover-card.stories.tsx +29 -4
- package/src/components/hover-card/hover-card.tsx +51 -13
- package/src/components/icon/DEVELOPMENT.md +809 -0
- package/src/components/icon/icon.meta.md +170 -0
- package/src/components/icon/icon.stories.tsx +344 -0
- package/src/components/icon/icon.tsx +248 -0
- package/src/components/image/image.meta.md +14 -18
- package/src/components/image/image.stories.tsx +3 -3
- package/src/components/image/image.tsx +2 -0
- package/src/components/input/input.meta.md +44 -43
- package/src/components/input/input.stories.tsx +62 -35
- package/src/components/input/input.tsx +96 -101
- package/src/components/input-group/input-group.meta.md +53 -39
- package/src/components/input-group/input-group.stories.tsx +49 -16
- package/src/components/input-group/input-group.tsx +45 -10
- package/src/components/input-number/input-number.meta.md +68 -20
- package/src/components/input-number/input-number.stories.tsx +33 -6
- package/src/components/input-number/input-number.tsx +81 -22
- package/src/components/input-otp/input-otp.meta.md +8 -20
- package/src/components/input-otp/input-otp.stories.tsx +3 -3
- package/src/components/input-otp/input-otp.tsx +1 -1
- package/src/components/item/item.meta.md +8 -26
- package/src/components/item/item.stories.tsx +3 -3
- package/src/components/item/item.tsx +7 -6
- package/src/components/kbd/kbd.meta.md +7 -19
- package/src/components/kbd/kbd.stories.tsx +4 -4
- package/src/components/kbd/kbd.tsx +8 -4
- package/src/components/label/label.meta.md +21 -18
- package/src/components/label/label.stories.tsx +64 -6
- package/src/components/label/label.tsx +91 -19
- package/src/components/masonry/masonry.meta.md +8 -12
- package/src/components/masonry/masonry.stories.tsx +4 -4
- package/src/components/mentions/mentions.meta.md +42 -21
- package/src/components/mentions/mentions.stories.tsx +120 -6
- package/src/components/mentions/mentions.tsx +10 -5
- package/src/components/menubar/menubar.meta.md +4 -8
- package/src/components/menubar/menubar.stories.tsx +55 -3
- package/src/components/menubar/menubar.tsx +9 -9
- package/src/components/native-select/native-select.meta.md +7 -11
- package/src/components/native-select/native-select.stories.tsx +4 -4
- package/src/components/native-select/native-select.tsx +2 -2
- package/src/components/navigation-menu/navigation-menu.meta.md +4 -8
- package/src/components/navigation-menu/navigation-menu.stories.tsx +106 -3
- package/src/components/navigation-menu/navigation-menu.tsx +6 -3
- package/src/components/notification/notification.meta.md +41 -8
- package/src/components/notification/notification.stories.tsx +9 -9
- package/src/components/notification/notification.tsx +34 -19
- package/src/components/page-header/DEVELOPMENT.md +842 -0
- package/src/components/page-header/page-header.meta.md +210 -0
- package/src/components/page-header/page-header.stories.tsx +428 -0
- package/src/components/page-header/page-header.tsx +284 -0
- package/src/components/page-shell/page-shell.meta.md +116 -0
- package/src/components/page-shell/page-shell.stories.tsx +149 -0
- package/src/components/page-shell/page-shell.tsx +115 -0
- package/src/components/pagination/pagination.meta.md +122 -50
- package/src/components/pagination/pagination.stories.tsx +227 -11
- package/src/components/pagination/pagination.tsx +345 -63
- package/src/components/popconfirm/popconfirm.meta.md +19 -23
- package/src/components/popconfirm/popconfirm.stories.tsx +2 -2
- package/src/components/popconfirm/popconfirm.tsx +1 -1
- package/src/components/popover/popover.meta.md +64 -12
- package/src/components/popover/popover.stories.tsx +83 -7
- package/src/components/popover/popover.tsx +77 -28
- package/src/components/progress/progress.meta.md +43 -26
- package/src/components/progress/progress.stories.tsx +2 -2
- package/src/components/progress/progress.tsx +19 -11
- package/src/components/radio-group/radio-group.meta.md +78 -11
- package/src/components/radio-group/radio-group.stories.tsx +38 -2
- package/src/components/radio-group/radio-group.tsx +149 -18
- package/src/components/rate/rate.meta.md +41 -19
- package/src/components/rate/rate.stories.tsx +2 -2
- package/src/components/rate/rate.tsx +37 -10
- package/src/components/resizable/resizable.meta.md +4 -12
- package/src/components/resizable/resizable.stories.tsx +5 -5
- package/src/components/resizable/resizable.tsx +1 -1
- package/src/components/result/result.meta.md +10 -14
- package/src/components/result/result.stories.tsx +2 -2
- package/src/components/result/result.tsx +21 -12
- package/src/components/scroll-area/scroll-area.meta.md +4 -8
- package/src/components/scroll-area/scroll-area.stories.tsx +5 -5
- package/src/components/segmented/segmented.meta.md +15 -17
- package/src/components/segmented/segmented.stories.tsx +3 -3
- package/src/components/segmented/segmented.tsx +16 -8
- package/src/components/select/select.meta.md +199 -67
- package/src/components/select/select.stories.tsx +238 -63
- package/src/components/select/select.tsx +718 -171
- package/src/components/separator/separator.meta.md +10 -14
- package/src/components/separator/separator.stories.tsx +2 -2
- package/src/components/separator/separator.tsx +3 -7
- package/src/components/sheet/sheet.meta.md +26 -21
- package/src/components/sheet/sheet.stories.tsx +116 -10
- package/src/components/sheet/sheet.tsx +116 -29
- package/src/components/sidebar/sidebar.meta.md +29 -38
- package/src/components/sidebar/sidebar.stories.tsx +696 -29
- package/src/components/sidebar/sidebar.tsx +643 -141
- package/src/components/skeleton/skeleton.meta.md +7 -31
- package/src/components/skeleton/skeleton.stories.tsx +3 -3
- package/src/components/skeleton/skeleton.tsx +7 -7
- package/src/components/slider/slider.meta.md +60 -13
- package/src/components/slider/slider.stories.tsx +58 -6
- package/src/components/slider/slider.tsx +154 -13
- package/src/components/sonner/sonner.meta.md +54 -8
- package/src/components/sonner/sonner.stories.tsx +79 -11
- package/src/components/sonner/sonner.tsx +137 -8
- package/src/components/spinner/spinner.meta.md +57 -21
- package/src/components/spinner/spinner.stories.tsx +66 -14
- package/src/components/spinner/spinner.tsx +111 -9
- package/src/components/statistic/statistic.meta.md +14 -30
- package/src/components/statistic/statistic.stories.tsx +1 -1
- package/src/components/statistic/statistic.tsx +4 -5
- package/src/components/steps/steps.meta.md +20 -15
- package/src/components/steps/steps.stories.tsx +37 -2
- package/src/components/steps/steps.tsx +15 -12
- package/src/components/switch/switch.meta.md +56 -15
- package/src/components/switch/switch.stories.tsx +5 -5
- package/src/components/switch/switch.tsx +59 -13
- package/src/components/table/table.meta.md +3 -7
- package/src/components/table/table.stories.tsx +1 -1
- package/src/components/table/table.tsx +8 -6
- package/src/components/tabs/tabs.meta.md +40 -32
- package/src/components/tabs/tabs.stories.tsx +104 -26
- package/src/components/tabs/tabs.tsx +125 -54
- package/src/components/tag/tag.meta.md +104 -68
- package/src/components/tag/tag.stories.tsx +183 -15
- package/src/components/tag/tag.tsx +222 -21
- package/src/components/textarea/textarea.meta.md +42 -31
- package/src/components/textarea/textarea.stories.tsx +32 -6
- package/src/components/textarea/textarea.tsx +32 -8
- package/src/components/time-picker/time-picker.meta.md +119 -50
- package/src/components/time-picker/time-picker.stories.tsx +65 -33
- package/src/components/time-picker/time-picker.tsx +889 -101
- package/src/components/timeline/timeline.meta.md +16 -17
- package/src/components/timeline/timeline.stories.tsx +24 -4
- package/src/components/timeline/timeline.tsx +32 -12
- package/src/components/toggle/toggle.meta.md +8 -12
- package/src/components/toggle/toggle.stories.tsx +4 -4
- package/src/components/toggle/toggle.tsx +4 -3
- package/src/components/toggle-group/toggle-group.meta.md +10 -14
- package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
- package/src/components/toggle-group/toggle-group.tsx +2 -2
- package/src/components/tooltip/tooltip.meta.md +63 -18
- package/src/components/tooltip/tooltip.stories.tsx +42 -5
- package/src/components/tooltip/tooltip.tsx +81 -21
- package/src/components/tour/tour.meta.md +16 -20
- package/src/components/tour/tour.stories.tsx +3 -3
- package/src/components/tour/tour.tsx +3 -3
- package/src/components/transfer/transfer.meta.md +18 -22
- package/src/components/transfer/transfer.stories.tsx +2 -2
- package/src/components/transfer/transfer.tsx +28 -21
- package/src/components/tree/tree.meta.md +67 -22
- package/src/components/tree/tree.stories.tsx +1 -1
- package/src/components/tree/tree.tsx +9 -8
- package/src/components/tree-select/tree-select.meta.md +59 -23
- package/src/components/tree-select/tree-select.stories.tsx +2 -2
- package/src/components/tree-select/tree-select.tsx +42 -7
- package/src/components/typography/typography.meta.md +61 -39
- package/src/components/typography/typography.stories.tsx +14 -9
- package/src/components/typography/typography.tsx +38 -25
- package/src/components/upload/upload.meta.md +61 -25
- package/src/components/upload/upload.stories.tsx +69 -3
- package/src/components/upload/upload.tsx +170 -37
- package/src/components/watermark/watermark.meta.md +15 -19
- package/src/components/watermark/watermark.stories.tsx +98 -8
- package/src/hooks/use-breakpoint.ts +117 -0
- package/src/hooks/use-debounce-callback.ts +52 -0
- package/src/hooks/use-mobile.ts +23 -0
- package/src/stories/theme-tokens.stories.tsx +747 -0
- package/src/utils/trigger-input.ts +57 -0
- package/src/components/button/demo/as-child.tsx +0 -24
- package/src/components/button/demo/basic.tsx +0 -8
- package/src/components/button/demo/block.tsx +0 -16
- package/src/components/button/demo/loading.tsx +0 -19
- package/src/components/button/demo/shapes.tsx +0 -18
- package/src/components/button/demo/sizes.tsx +0 -19
- package/src/components/button/demo/variants.tsx +0 -19
- package/src/components/button/demo/with-icon.tsx +0 -20
- package/src/components/button-group/button-group.meta.md +0 -101
- package/src/components/button-group/button-group.stories.tsx +0 -93
- package/src/components/button-group/button-group.tsx +0 -75
- package/src/components/combobox/combobox.meta.md +0 -102
- package/src/components/combobox/combobox.stories.tsx +0 -55
- package/src/components/combobox/combobox.tsx +0 -130
- package/src/components/input/demo/addon.tsx +0 -15
- package/src/components/input/demo/basic.tsx +0 -12
- package/src/components/input/demo/clearable.tsx +0 -21
- package/src/components/input/demo/show-count.tsx +0 -18
- package/src/components/input/demo/sizes.tsx +0 -15
- package/src/components/input/demo/with-prefix-suffix.tsx +0 -19
- package/src/components/space/space.meta.md +0 -103
- package/src/components/space/space.stories.tsx +0 -108
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
51
|
-
const
|
|
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
|
-
(
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
[
|
|
123
|
+
[setOpenProp, open],
|
|
60
124
|
);
|
|
61
|
-
const toggle = React.useCallback(() => setOpen(!open), [open, setOpen]);
|
|
62
125
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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={
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
207
|
+
const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
|
|
122
208
|
(
|
|
123
209
|
{
|
|
124
210
|
side = 'left',
|
|
125
|
-
|
|
126
|
-
|
|
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 {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
<
|
|
264
|
+
<div
|
|
146
265
|
ref={ref}
|
|
147
|
-
|
|
148
|
-
data-
|
|
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
|
-
|
|
151
|
-
className={cn(
|
|
152
|
-
sidebarVariants({ side }),
|
|
153
|
-
collapsible === 'offcanvas' && !open && 'overflow-hidden',
|
|
154
|
-
className,
|
|
155
|
-
)}
|
|
156
|
-
{...props}
|
|
271
|
+
data-slot="sidebar"
|
|
157
272
|
>
|
|
158
|
-
{
|
|
159
|
-
|
|
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
|
-
// ───
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
>(({ className, ...props }, ref) => {
|
|
234
|
-
const
|
|
499
|
+
SidebarGroupLabelProps
|
|
500
|
+
>(({ className, asChild = false, ...props }, ref) => {
|
|
501
|
+
const Comp = asChild ? Slot : 'div';
|
|
235
502
|
return (
|
|
236
|
-
<
|
|
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
|
|
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
|
|
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
|
-
>(
|
|
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-
|
|
686
|
+
data-slot="sidebar-menu-action"
|
|
687
|
+
data-sidebar="menu-action"
|
|
286
688
|
className={cn(
|
|
287
|
-
'
|
|
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
|
-
|
|
700
|
+
SidebarMenuAction.displayName = 'SidebarMenuAction';
|
|
295
701
|
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
<
|
|
734
|
+
<div
|
|
305
735
|
ref={ref}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
className={cn('
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
759
|
+
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
|
|
322
760
|
|
|
323
|
-
// ───
|
|
761
|
+
// ─── MenuSub / MenuSubItem / MenuSubButton ─────────────────────────────────
|
|
324
762
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
React.HTMLAttributes<
|
|
763
|
+
const SidebarMenuSub = React.forwardRef<
|
|
764
|
+
HTMLUListElement,
|
|
765
|
+
React.HTMLAttributes<HTMLUListElement>
|
|
328
766
|
>(({ className, ...props }, ref) => (
|
|
329
|
-
<
|
|
767
|
+
<ul
|
|
330
768
|
ref={ref}
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|