@emara/ui 1.1.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/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
cloneElement,
|
|
6
|
+
createContext,
|
|
7
|
+
forwardRef,
|
|
8
|
+
isValidElement,
|
|
9
|
+
useContext,
|
|
10
|
+
useMemo,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from "react";
|
|
15
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
16
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
17
|
+
import { RiArrowDownSLine, RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
|
|
18
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
19
|
+
|
|
20
|
+
import { cn } from "@/lib/utils";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
22
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
23
|
+
|
|
24
|
+
// Per docs/emara-ui-phase-5-components.md §2.
|
|
25
|
+
|
|
26
|
+
// ---- Context ---------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
type SidebarVariant = "filled" | "outline" | "floating";
|
|
29
|
+
|
|
30
|
+
interface SidebarContextValue {
|
|
31
|
+
variant: SidebarVariant;
|
|
32
|
+
collapsed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SidebarContext = createContext<SidebarContextValue | null>(null);
|
|
36
|
+
|
|
37
|
+
function useSidebar(): SidebarContextValue {
|
|
38
|
+
const ctx = useContext(SidebarContext);
|
|
39
|
+
if (!ctx) throw new Error("Sidebar subcomponents must be used inside <Sidebar>");
|
|
40
|
+
return ctx;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Root ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
// Width is driven by tokens (--sidebar-w-{sm,md,lg,collapsed}) defined in
|
|
46
|
+
// design-tokens §11b.3. Collapsed width is fixed across sizes.
|
|
47
|
+
const sidebarVariants = cva(
|
|
48
|
+
"flex h-full flex-col text-foreground transition-[width] duration-normal",
|
|
49
|
+
{
|
|
50
|
+
variants: {
|
|
51
|
+
variant: {
|
|
52
|
+
filled: "bg-muted",
|
|
53
|
+
outline: "bg-background border-e border-border",
|
|
54
|
+
floating: "bg-card rounded-lg shadow-md m-2",
|
|
55
|
+
},
|
|
56
|
+
size: {
|
|
57
|
+
sm: "",
|
|
58
|
+
md: "",
|
|
59
|
+
lg: "",
|
|
60
|
+
},
|
|
61
|
+
collapsed: {
|
|
62
|
+
true: "w-sidebar-collapsed",
|
|
63
|
+
false: "",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
compoundVariants: [
|
|
67
|
+
// Expanded width — driven by `size` only when not collapsed.
|
|
68
|
+
{ collapsed: false, size: "sm", class: "w-sidebar-sm" },
|
|
69
|
+
{ collapsed: false, size: "md", class: "w-sidebar-md" },
|
|
70
|
+
{ collapsed: false, size: "lg", class: "w-sidebar-lg" },
|
|
71
|
+
],
|
|
72
|
+
defaultVariants: {
|
|
73
|
+
variant: "filled",
|
|
74
|
+
size: "md",
|
|
75
|
+
collapsed: false,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
type SidebarVariants = VariantProps<typeof sidebarVariants>;
|
|
81
|
+
|
|
82
|
+
type SidebarProps = React.HTMLAttributes<HTMLElement> &
|
|
83
|
+
SidebarVariants & {
|
|
84
|
+
asChild?: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const Sidebar = forwardRef<HTMLElement, SidebarProps>(function Sidebar(
|
|
88
|
+
{ className, variant = "filled", size = "md", collapsed = false, asChild = false, ...props },
|
|
89
|
+
ref,
|
|
90
|
+
) {
|
|
91
|
+
const Comp = (asChild ? Slot : "aside") as React.ElementType;
|
|
92
|
+
const ctx = useMemo<SidebarContextValue>(
|
|
93
|
+
() => ({
|
|
94
|
+
variant: variant ?? "filled",
|
|
95
|
+
collapsed: collapsed ?? false,
|
|
96
|
+
}),
|
|
97
|
+
[variant, collapsed],
|
|
98
|
+
);
|
|
99
|
+
return (
|
|
100
|
+
<SidebarContext.Provider value={ctx}>
|
|
101
|
+
<Comp
|
|
102
|
+
ref={ref}
|
|
103
|
+
data-collapsed={collapsed || undefined}
|
|
104
|
+
data-variant={variant}
|
|
105
|
+
data-size={size}
|
|
106
|
+
className={cn(sidebarVariants({ variant, size, collapsed }), className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
</SidebarContext.Provider>
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
Sidebar.displayName = "Sidebar";
|
|
113
|
+
|
|
114
|
+
// ---- Brand / Header / Body / Footer ---------------------------------------
|
|
115
|
+
|
|
116
|
+
const SidebarBrand = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
117
|
+
function SidebarBrand({ className, ...props }, ref) {
|
|
118
|
+
const { collapsed } = useSidebar();
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cn(
|
|
123
|
+
"text-foreground flex h-14 shrink-0 items-center gap-2 px-3 font-semibold",
|
|
124
|
+
"[&_svg]:size-6 [&_svg]:shrink-0",
|
|
125
|
+
collapsed && "justify-center",
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
SidebarBrand.displayName = "SidebarBrand";
|
|
134
|
+
|
|
135
|
+
const SidebarHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
136
|
+
function SidebarHeader({ className, ...props }, ref) {
|
|
137
|
+
return <div ref={ref} className={cn("shrink-0 px-3 pb-2", className)} {...props} />;
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
SidebarHeader.displayName = "SidebarHeader";
|
|
141
|
+
|
|
142
|
+
const SidebarBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
143
|
+
function SidebarBody({ className, ...props }, ref) {
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
ref={ref}
|
|
147
|
+
className={cn("flex-1 space-y-3 overflow-x-hidden overflow-y-auto px-2 py-2", className)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
SidebarBody.displayName = "SidebarBody";
|
|
154
|
+
|
|
155
|
+
const SidebarFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
156
|
+
function SidebarFooter({ className, ...props }, ref) {
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
ref={ref}
|
|
160
|
+
className={cn("border-border/60 shrink-0 border-t px-2 py-2", className)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
SidebarFooter.displayName = "SidebarFooter";
|
|
167
|
+
|
|
168
|
+
// ---- Group -----------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const SidebarGroup = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
171
|
+
function SidebarGroup({ className, ...props }, ref) {
|
|
172
|
+
return <div ref={ref} className={cn("space-y-1", className)} {...props} />;
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
SidebarGroup.displayName = "SidebarGroup";
|
|
176
|
+
|
|
177
|
+
const SidebarGroupLabel = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
178
|
+
function SidebarGroupLabel({ className, ...props }, ref) {
|
|
179
|
+
const { collapsed } = useSidebar();
|
|
180
|
+
if (collapsed) {
|
|
181
|
+
return <div ref={ref} aria-hidden="true" className="h-2" />;
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
ref={ref}
|
|
186
|
+
className={cn(
|
|
187
|
+
"text-muted-foreground px-3 pb-1 text-xs font-medium tracking-wider uppercase",
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
|
196
|
+
|
|
197
|
+
// ---- Item visual ----------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
const itemVariants = cva(
|
|
200
|
+
[
|
|
201
|
+
"group flex w-full items-center gap-2 rounded-md ps-3 pe-2 text-sm cursor-pointer",
|
|
202
|
+
"text-foreground transition-colors select-none",
|
|
203
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
204
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
205
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
206
|
+
"data-[active=true]:bg-accent data-[active=true]:text-accent-foreground data-[active=true]:font-medium",
|
|
207
|
+
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
|
208
|
+
].join(" "),
|
|
209
|
+
{
|
|
210
|
+
variants: {
|
|
211
|
+
collapsed: {
|
|
212
|
+
true: "justify-center px-0 h-10 w-10 mx-auto",
|
|
213
|
+
false: "h-9",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
defaultVariants: { collapsed: false },
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Inner item content — used by both flat Items and the trigger of expandable Items
|
|
221
|
+
function ItemContent({
|
|
222
|
+
icon,
|
|
223
|
+
label,
|
|
224
|
+
badge,
|
|
225
|
+
kbd,
|
|
226
|
+
expandable,
|
|
227
|
+
expanded,
|
|
228
|
+
collapsed,
|
|
229
|
+
}: {
|
|
230
|
+
icon?: ReactNode;
|
|
231
|
+
label: ReactNode;
|
|
232
|
+
badge?: ReactNode;
|
|
233
|
+
kbd?: string[];
|
|
234
|
+
expandable?: boolean;
|
|
235
|
+
expanded?: boolean;
|
|
236
|
+
collapsed: boolean;
|
|
237
|
+
}) {
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
{icon ? <span className="shrink-0">{icon}</span> : null}
|
|
241
|
+
<span className={cn("flex-1 truncate text-start", collapsed && "sr-only")}>{label}</span>
|
|
242
|
+
{!collapsed && badge ? <span className="ms-1 shrink-0">{badge}</span> : null}
|
|
243
|
+
{!collapsed && kbd && kbd.length > 0 ? (
|
|
244
|
+
<kbd className="border-border bg-muted text-muted-foreground ms-1 inline-flex items-center gap-0.5 rounded border px-1.5 py-0.5 text-[10px] font-medium">
|
|
245
|
+
{kbd.map((k, i) => (
|
|
246
|
+
<span key={i}>{k}</span>
|
|
247
|
+
))}
|
|
248
|
+
</kbd>
|
|
249
|
+
) : null}
|
|
250
|
+
{!collapsed && expandable ? (
|
|
251
|
+
<span
|
|
252
|
+
aria-hidden="true"
|
|
253
|
+
className={cn(
|
|
254
|
+
"text-muted-foreground ms-1 shrink-0 transition-transform",
|
|
255
|
+
expanded && "rotate-180",
|
|
256
|
+
)}
|
|
257
|
+
>
|
|
258
|
+
<RiArrowDownSLine />
|
|
259
|
+
</span>
|
|
260
|
+
) : null}
|
|
261
|
+
</>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- Item ------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
type SidebarItemBaseProps = React.HTMLAttributes<HTMLElement> & {
|
|
268
|
+
icon?: ReactNode;
|
|
269
|
+
label?: ReactNode;
|
|
270
|
+
badge?: ReactNode;
|
|
271
|
+
kbd?: string[];
|
|
272
|
+
tooltip?: string;
|
|
273
|
+
active?: boolean;
|
|
274
|
+
disabled?: boolean;
|
|
275
|
+
asChild?: boolean;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
type SidebarItemFlatProps = SidebarItemBaseProps & {
|
|
279
|
+
expandable?: false;
|
|
280
|
+
/** Required for expandable=true. Ignored when flat. */
|
|
281
|
+
defaultExpanded?: boolean;
|
|
282
|
+
children?: ReactNode;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
type SidebarItemExpandableProps = SidebarItemBaseProps & {
|
|
286
|
+
expandable: true;
|
|
287
|
+
defaultExpanded?: boolean;
|
|
288
|
+
/** Nested SidebarSubItems. */
|
|
289
|
+
children?: ReactNode;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
type SidebarItemProps = SidebarItemFlatProps | SidebarItemExpandableProps;
|
|
293
|
+
|
|
294
|
+
const SidebarItem = forwardRef<HTMLElement, SidebarItemProps>(function SidebarItem(
|
|
295
|
+
{
|
|
296
|
+
className,
|
|
297
|
+
icon,
|
|
298
|
+
label,
|
|
299
|
+
badge,
|
|
300
|
+
kbd,
|
|
301
|
+
tooltip,
|
|
302
|
+
active = false,
|
|
303
|
+
disabled = false,
|
|
304
|
+
asChild = false,
|
|
305
|
+
children,
|
|
306
|
+
...rest
|
|
307
|
+
},
|
|
308
|
+
ref,
|
|
309
|
+
) {
|
|
310
|
+
const expandable = "expandable" in rest && rest.expandable === true;
|
|
311
|
+
const defaultExpanded =
|
|
312
|
+
"defaultExpanded" in rest && rest.defaultExpanded ? Boolean(rest.defaultExpanded) : false;
|
|
313
|
+
// Strip discriminator keys before spreading onto DOM.
|
|
314
|
+
const domProps: Record<string, unknown> = { ...rest };
|
|
315
|
+
delete domProps.expandable;
|
|
316
|
+
delete domProps.defaultExpanded;
|
|
317
|
+
|
|
318
|
+
const { collapsed } = useSidebar();
|
|
319
|
+
|
|
320
|
+
// Use label as accessible tooltip text when none provided + collapsed mode.
|
|
321
|
+
const tooltipText = tooltip ?? (typeof label === "string" ? label : undefined);
|
|
322
|
+
|
|
323
|
+
// --- Expandable ----------------------------------------------------------
|
|
324
|
+
if (expandable) {
|
|
325
|
+
return (
|
|
326
|
+
<ExpandableItem
|
|
327
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
328
|
+
defaultExpanded={defaultExpanded}
|
|
329
|
+
active={active}
|
|
330
|
+
disabled={disabled}
|
|
331
|
+
collapsed={collapsed}
|
|
332
|
+
{...(icon !== undefined ? { icon } : {})}
|
|
333
|
+
label={label}
|
|
334
|
+
{...(badge !== undefined ? { badge } : {})}
|
|
335
|
+
{...(kbd !== undefined ? { kbd } : {})}
|
|
336
|
+
{...(tooltipText !== undefined ? { tooltip: tooltipText } : {})}
|
|
337
|
+
{...(className !== undefined ? { className } : {})}
|
|
338
|
+
{...(domProps as React.HTMLAttributes<HTMLDivElement>)}
|
|
339
|
+
>
|
|
340
|
+
{children}
|
|
341
|
+
</ExpandableItem>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- Flat item -----------------------------------------------------------
|
|
346
|
+
const sharedProps = {
|
|
347
|
+
"data-active": active || undefined,
|
|
348
|
+
"data-disabled": disabled || undefined,
|
|
349
|
+
...(disabled ? { "aria-disabled": true, tabIndex: -1 } : {}),
|
|
350
|
+
...(active ? { "aria-current": "page" as const } : {}),
|
|
351
|
+
className: cn(itemVariants({ collapsed }), className),
|
|
352
|
+
...domProps,
|
|
353
|
+
};
|
|
354
|
+
const itemContent = (
|
|
355
|
+
<ItemContent
|
|
356
|
+
{...(icon !== undefined ? { icon } : {})}
|
|
357
|
+
label={label}
|
|
358
|
+
{...(badge !== undefined ? { badge } : {})}
|
|
359
|
+
{...(kbd !== undefined ? { kbd } : {})}
|
|
360
|
+
collapsed={collapsed}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
let triggerEl: ReactNode;
|
|
365
|
+
if (asChild) {
|
|
366
|
+
// Clone the consumer's single child (an `<a>`, Next `<Link>`, etc.),
|
|
367
|
+
// merge our props onto it, and inject ItemContent as its body. Any
|
|
368
|
+
// existing children on the consumer's element are preserved before
|
|
369
|
+
// ItemContent so consumers can compose if they want.
|
|
370
|
+
const child = Children.only(children) as ReactElement<{
|
|
371
|
+
className?: string;
|
|
372
|
+
ref?: React.Ref<unknown>;
|
|
373
|
+
children?: ReactNode;
|
|
374
|
+
}>;
|
|
375
|
+
if (!isValidElement(child)) {
|
|
376
|
+
throw new Error("SidebarItem with asChild expects a single valid React element.");
|
|
377
|
+
}
|
|
378
|
+
triggerEl = cloneElement(
|
|
379
|
+
child,
|
|
380
|
+
{
|
|
381
|
+
...sharedProps,
|
|
382
|
+
className: cn(sharedProps.className, child.props.className),
|
|
383
|
+
ref,
|
|
384
|
+
},
|
|
385
|
+
child.props.children ?? itemContent,
|
|
386
|
+
);
|
|
387
|
+
} else {
|
|
388
|
+
triggerEl = (
|
|
389
|
+
<button ref={ref as React.Ref<HTMLButtonElement>} type="button" {...sharedProps}>
|
|
390
|
+
{itemContent}
|
|
391
|
+
</button>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (collapsed && tooltipText) {
|
|
396
|
+
// `side="right"` is correct for a start-positioned sidebar in both
|
|
397
|
+
// LTR (toward content on the right) and RTL (Radix auto-flips to
|
|
398
|
+
// "left", which is toward content). For an end-positioned sidebar
|
|
399
|
+
// the tooltip would point off-screen — a known limitation tracked
|
|
400
|
+
// for v1.x once Sidebar can read its parent AppShell's position.
|
|
401
|
+
return (
|
|
402
|
+
<Tooltip>
|
|
403
|
+
<TooltipTrigger asChild>{triggerEl}</TooltipTrigger>
|
|
404
|
+
<TooltipContent side="right">{tooltipText}</TooltipContent>
|
|
405
|
+
</Tooltip>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return triggerEl;
|
|
410
|
+
});
|
|
411
|
+
SidebarItem.displayName = "SidebarItem";
|
|
412
|
+
|
|
413
|
+
// ---- ExpandableItem (internal) --------------------------------------------
|
|
414
|
+
|
|
415
|
+
const ExpandableItem = forwardRef<
|
|
416
|
+
HTMLDivElement,
|
|
417
|
+
Omit<SidebarItemBaseProps, "tooltip"> & {
|
|
418
|
+
defaultExpanded: boolean;
|
|
419
|
+
tooltip?: string;
|
|
420
|
+
collapsed: boolean;
|
|
421
|
+
children?: ReactNode;
|
|
422
|
+
}
|
|
423
|
+
>(function ExpandableItem(
|
|
424
|
+
{
|
|
425
|
+
defaultExpanded,
|
|
426
|
+
icon,
|
|
427
|
+
label,
|
|
428
|
+
badge,
|
|
429
|
+
kbd,
|
|
430
|
+
tooltip,
|
|
431
|
+
active,
|
|
432
|
+
disabled,
|
|
433
|
+
className,
|
|
434
|
+
collapsed,
|
|
435
|
+
children,
|
|
436
|
+
...props
|
|
437
|
+
},
|
|
438
|
+
ref,
|
|
439
|
+
) {
|
|
440
|
+
const [open, setOpen] = useState(defaultExpanded);
|
|
441
|
+
|
|
442
|
+
// Collapsed mode: open as a popover instead of inline collapsible.
|
|
443
|
+
if (collapsed) {
|
|
444
|
+
return (
|
|
445
|
+
<Popover>
|
|
446
|
+
<PopoverTrigger asChild>
|
|
447
|
+
<button
|
|
448
|
+
ref={ref as unknown as React.Ref<HTMLButtonElement>}
|
|
449
|
+
type="button"
|
|
450
|
+
data-active={active || undefined}
|
|
451
|
+
data-disabled={disabled || undefined}
|
|
452
|
+
{...(disabled ? { "aria-disabled": true, tabIndex: -1 } : {})}
|
|
453
|
+
className={cn(itemVariants({ collapsed: true }), className)}
|
|
454
|
+
>
|
|
455
|
+
<ItemContent
|
|
456
|
+
{...(icon !== undefined ? { icon } : {})}
|
|
457
|
+
label={label}
|
|
458
|
+
{...(badge !== undefined ? { badge } : {})}
|
|
459
|
+
{...(kbd !== undefined ? { kbd } : {})}
|
|
460
|
+
expandable
|
|
461
|
+
collapsed
|
|
462
|
+
/>
|
|
463
|
+
</button>
|
|
464
|
+
</PopoverTrigger>
|
|
465
|
+
<PopoverContent side="right" align="start" className="w-56 p-1">
|
|
466
|
+
{tooltip ? (
|
|
467
|
+
<div className="text-muted-foreground px-2 py-1.5 text-xs font-medium">{tooltip}</div>
|
|
468
|
+
) : null}
|
|
469
|
+
<div className="space-y-0.5">{children}</div>
|
|
470
|
+
</PopoverContent>
|
|
471
|
+
</Popover>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<CollapsiblePrimitive.Root open={open} onOpenChange={setOpen} asChild>
|
|
477
|
+
<div ref={ref} className="contents">
|
|
478
|
+
<CollapsiblePrimitive.Trigger asChild>
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
data-active={active || undefined}
|
|
482
|
+
data-disabled={disabled || undefined}
|
|
483
|
+
{...(disabled ? { "aria-disabled": true, tabIndex: -1 } : {})}
|
|
484
|
+
className={cn(itemVariants({ collapsed: false }), className)}
|
|
485
|
+
{...props}
|
|
486
|
+
>
|
|
487
|
+
<ItemContent
|
|
488
|
+
{...(icon !== undefined ? { icon } : {})}
|
|
489
|
+
label={label}
|
|
490
|
+
{...(badge !== undefined ? { badge } : {})}
|
|
491
|
+
{...(kbd !== undefined ? { kbd } : {})}
|
|
492
|
+
expandable
|
|
493
|
+
expanded={open}
|
|
494
|
+
collapsed={false}
|
|
495
|
+
/>
|
|
496
|
+
</button>
|
|
497
|
+
</CollapsiblePrimitive.Trigger>
|
|
498
|
+
<CollapsiblePrimitive.Content
|
|
499
|
+
className={cn(
|
|
500
|
+
"overflow-hidden",
|
|
501
|
+
"data-[state=open]:animate-[collapse-down_var(--duration-fast,150ms)_var(--ease-out,ease-out)]",
|
|
502
|
+
"data-[state=closed]:animate-[collapse-up_var(--duration-fast,150ms)_var(--ease-in,ease-in)]",
|
|
503
|
+
"[--collapsible-content-height:var(--radix-collapsible-content-height)]",
|
|
504
|
+
)}
|
|
505
|
+
>
|
|
506
|
+
<div className="border-border ms-3 mt-0.5 space-y-0.5 border-s ps-3">{children}</div>
|
|
507
|
+
</CollapsiblePrimitive.Content>
|
|
508
|
+
</div>
|
|
509
|
+
</CollapsiblePrimitive.Root>
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
ExpandableItem.displayName = "ExpandableItem";
|
|
513
|
+
|
|
514
|
+
// ---- SubItem ---------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
type SidebarSubItemProps = Omit<SidebarItemBaseProps, "tooltip"> & {
|
|
517
|
+
children?: ReactNode;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const SidebarSubItem = forwardRef<HTMLElement, SidebarSubItemProps>(function SidebarSubItem(
|
|
521
|
+
{
|
|
522
|
+
className,
|
|
523
|
+
icon,
|
|
524
|
+
label,
|
|
525
|
+
badge,
|
|
526
|
+
kbd,
|
|
527
|
+
active = false,
|
|
528
|
+
disabled = false,
|
|
529
|
+
asChild = false,
|
|
530
|
+
...rest
|
|
531
|
+
},
|
|
532
|
+
ref,
|
|
533
|
+
) {
|
|
534
|
+
const Comp = (asChild ? Slot : "button") as React.ElementType;
|
|
535
|
+
return (
|
|
536
|
+
<Comp
|
|
537
|
+
ref={ref}
|
|
538
|
+
data-active={active || undefined}
|
|
539
|
+
data-disabled={disabled || undefined}
|
|
540
|
+
{...(disabled ? { "aria-disabled": true, tabIndex: -1 } : {})}
|
|
541
|
+
{...(active ? { "aria-current": "page" } : {})}
|
|
542
|
+
{...(!asChild ? { type: "button" as const } : {})}
|
|
543
|
+
className={cn(
|
|
544
|
+
// Slightly smaller indent; reuses item variants for color logic.
|
|
545
|
+
"group flex h-8 w-full cursor-pointer items-center gap-2 rounded-md ps-3 pe-2 text-sm",
|
|
546
|
+
"text-muted-foreground transition-colors select-none",
|
|
547
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
548
|
+
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
|
|
549
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
550
|
+
"data-[active=true]:bg-accent data-[active=true]:text-accent-foreground data-[active=true]:font-medium",
|
|
551
|
+
"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
|
552
|
+
className,
|
|
553
|
+
)}
|
|
554
|
+
{...rest}
|
|
555
|
+
>
|
|
556
|
+
{icon ? <span className="shrink-0">{icon}</span> : null}
|
|
557
|
+
<span className="flex-1 truncate text-start">{label}</span>
|
|
558
|
+
{badge ? <span className="ms-1 shrink-0">{badge}</span> : null}
|
|
559
|
+
{kbd && kbd.length > 0 ? (
|
|
560
|
+
<kbd className="border-border bg-muted text-muted-foreground ms-1 inline-flex items-center gap-0.5 rounded border px-1.5 py-0.5 text-[10px] font-medium">
|
|
561
|
+
{kbd.map((k, i) => (
|
|
562
|
+
<span key={i}>{k}</span>
|
|
563
|
+
))}
|
|
564
|
+
</kbd>
|
|
565
|
+
) : null}
|
|
566
|
+
</Comp>
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
SidebarSubItem.displayName = "SidebarSubItem";
|
|
570
|
+
|
|
571
|
+
// ---- CollapseToggle --------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
interface SidebarCollapseToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
574
|
+
/** Override the auto-injected aria-label. */
|
|
575
|
+
expandLabel?: string;
|
|
576
|
+
collapseLabel?: string;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const SidebarCollapseToggle = forwardRef<HTMLButtonElement, SidebarCollapseToggleProps>(
|
|
580
|
+
function SidebarCollapseToggle(
|
|
581
|
+
{ className, expandLabel = "Expand sidebar", collapseLabel = "Collapse sidebar", ...props },
|
|
582
|
+
ref,
|
|
583
|
+
) {
|
|
584
|
+
const { collapsed } = useSidebar();
|
|
585
|
+
return (
|
|
586
|
+
<button
|
|
587
|
+
ref={ref}
|
|
588
|
+
type="button"
|
|
589
|
+
aria-label={collapsed ? expandLabel : collapseLabel}
|
|
590
|
+
className={cn(
|
|
591
|
+
"inline-flex h-8 w-full cursor-pointer items-center justify-center rounded-md",
|
|
592
|
+
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
593
|
+
"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
|
|
594
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
595
|
+
className,
|
|
596
|
+
)}
|
|
597
|
+
{...props}
|
|
598
|
+
>
|
|
599
|
+
{collapsed ? (
|
|
600
|
+
<RiArrowRightSLine className="rtl-mirror" />
|
|
601
|
+
) : (
|
|
602
|
+
<RiArrowLeftSLine className="rtl-mirror" />
|
|
603
|
+
)}
|
|
604
|
+
</button>
|
|
605
|
+
);
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
SidebarCollapseToggle.displayName = "SidebarCollapseToggle";
|
|
609
|
+
|
|
610
|
+
export {
|
|
611
|
+
Sidebar,
|
|
612
|
+
SidebarBrand,
|
|
613
|
+
SidebarHeader,
|
|
614
|
+
SidebarBody,
|
|
615
|
+
SidebarFooter,
|
|
616
|
+
SidebarGroup,
|
|
617
|
+
SidebarGroupLabel,
|
|
618
|
+
SidebarItem,
|
|
619
|
+
SidebarSubItem,
|
|
620
|
+
SidebarCollapseToggle,
|
|
621
|
+
sidebarVariants,
|
|
622
|
+
};
|
|
623
|
+
export type { SidebarProps, SidebarItemProps, SidebarSubItemProps, SidebarCollapseToggleProps };
|