@multicomma/arkcn 0.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/README.md +78 -0
- package/bin/arkcn.mjs +78 -0
- package/cli/commands.mjs +251 -0
- package/cli/project.mjs +168 -0
- package/cli/registry.mjs +50 -0
- package/cli/ui.mjs +46 -0
- package/mcp/server.mjs +149 -0
- package/package.json +201 -0
- package/registry/base.css +680 -0
- package/registry/index.json +1833 -0
- package/registry/items/accordion.json +18 -0
- package/registry/items/activity-feed.json +18 -0
- package/registry/items/alert-dialog.json +18 -0
- package/registry/items/alert.json +17 -0
- package/registry/items/angle-slider.json +17 -0
- package/registry/items/app-shell.json +28 -0
- package/registry/items/aspect-ratio.json +13 -0
- package/registry/items/attachment.json +19 -0
- package/registry/items/avatar.json +17 -0
- package/registry/items/badge.json +18 -0
- package/registry/items/breadcrumb.json +18 -0
- package/registry/items/bubble.json +18 -0
- package/registry/items/button-group.json +19 -0
- package/registry/items/button.json +18 -0
- package/registry/items/calendar.json +19 -0
- package/registry/items/canvas.json +24 -0
- package/registry/items/card.json +15 -0
- package/registry/items/carousel.json +19 -0
- package/registry/items/cascader.json +23 -0
- package/registry/items/chart.json +17 -0
- package/registry/items/chat.json +23 -0
- package/registry/items/checkbox.json +18 -0
- package/registry/items/clipboard.json +19 -0
- package/registry/items/collapsible.json +15 -0
- package/registry/items/color-picker.json +19 -0
- package/registry/items/combobox.json +20 -0
- package/registry/items/command.json +21 -0
- package/registry/items/comment-thread.json +25 -0
- package/registry/items/context-menu.json +18 -0
- package/registry/items/controllable.json +14 -0
- package/registry/items/data-grid.json +24 -0
- package/registry/items/data-table-adapter.json +14 -0
- package/registry/items/data-table.json +32 -0
- package/registry/items/date-input.json +17 -0
- package/registry/items/dialog.json +19 -0
- package/registry/items/direction.json +13 -0
- package/registry/items/drag-preview.json +16 -0
- package/registry/items/drawer.json +18 -0
- package/registry/items/dropdown-menu.json +18 -0
- package/registry/items/editable.json +17 -0
- package/registry/items/empty.json +17 -0
- package/registry/items/field.json +19 -0
- package/registry/items/file-upload.json +19 -0
- package/registry/items/floating-panel.json +19 -0
- package/registry/items/floating-toolbar.json +16 -0
- package/registry/items/form.json +14 -0
- package/registry/items/format.json +15 -0
- package/registry/items/gantt.json +22 -0
- package/registry/items/history.json +14 -0
- package/registry/items/hotkeys.json +18 -0
- package/registry/items/hover-card.json +17 -0
- package/registry/items/image-cropper.json +17 -0
- package/registry/items/input-group.json +20 -0
- package/registry/items/input-otp.json +18 -0
- package/registry/items/input.json +15 -0
- package/registry/items/item.json +19 -0
- package/registry/items/json-tree-view.json +17 -0
- package/registry/items/kanban.json +26 -0
- package/registry/items/kbd.json +15 -0
- package/registry/items/label.json +15 -0
- package/registry/items/live-region.json +16 -0
- package/registry/items/marker.json +18 -0
- package/registry/items/marquee.json +17 -0
- package/registry/items/menubar.json +19 -0
- package/registry/items/message.json +15 -0
- package/registry/items/navigation-menu.json +19 -0
- package/registry/items/node-graph.json +22 -0
- package/registry/items/number-input.json +18 -0
- package/registry/items/pagination.json +18 -0
- package/registry/items/password-input.json +18 -0
- package/registry/items/popconfirm.json +20 -0
- package/registry/items/popover.json +18 -0
- package/registry/items/progress.json +17 -0
- package/registry/items/qr-code.json +17 -0
- package/registry/items/query-builder.json +23 -0
- package/registry/items/radio-group.json +17 -0
- package/registry/items/rating-group.json +18 -0
- package/registry/items/resizable.json +17 -0
- package/registry/items/rich-text-editor.json +37 -0
- package/registry/items/row-layout.json +15 -0
- package/registry/items/scheduler.json +23 -0
- package/registry/items/scroll-area.json +17 -0
- package/registry/items/segment-group.json +17 -0
- package/registry/items/select.json +19 -0
- package/registry/items/separator.json +15 -0
- package/registry/items/sheet.json +19 -0
- package/registry/items/sidebar.json +27 -0
- package/registry/items/signature-pad.json +19 -0
- package/registry/items/skeleton.json +15 -0
- package/registry/items/slider.json +17 -0
- package/registry/items/sonner.json +17 -0
- package/registry/items/spinner.json +17 -0
- package/registry/items/steps.json +19 -0
- package/registry/items/swap.json +17 -0
- package/registry/items/switch.json +17 -0
- package/registry/items/table.json +16 -0
- package/registry/items/tabs.json +18 -0
- package/registry/items/tags-input.json +18 -0
- package/registry/items/textarea.json +15 -0
- package/registry/items/time.json +14 -0
- package/registry/items/timer.json +18 -0
- package/registry/items/toc.json +17 -0
- package/registry/items/toggle-group.json +19 -0
- package/registry/items/toggle.json +18 -0
- package/registry/items/tooltip.json +17 -0
- package/registry/items/tour.json +19 -0
- package/registry/items/transfer-list.json +24 -0
- package/registry/items/tree-select.json +24 -0
- package/registry/items/tree-view.json +18 -0
- package/registry/items/use-mobile.json +13 -0
- package/registry/items/utils.json +17 -0
- package/registry/items/virtual-list.json +16 -0
- package/src/components/ui/accordion.tsx +111 -0
- package/src/components/ui/activity-feed.tsx +228 -0
- package/src/components/ui/alert-dialog.tsx +204 -0
- package/src/components/ui/alert.tsx +55 -0
- package/src/components/ui/angle-slider.tsx +117 -0
- package/src/components/ui/app-shell.tsx +985 -0
- package/src/components/ui/aspect-ratio.tsx +35 -0
- package/src/components/ui/attachment.tsx +193 -0
- package/src/components/ui/avatar.tsx +94 -0
- package/src/components/ui/badge.tsx +45 -0
- package/src/components/ui/breadcrumb.tsx +93 -0
- package/src/components/ui/bubble.tsx +119 -0
- package/src/components/ui/button-group.tsx +79 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/calendar.tsx +172 -0
- package/src/components/ui/canvas.tsx +723 -0
- package/src/components/ui/card.tsx +69 -0
- package/src/components/ui/carousel.tsx +228 -0
- package/src/components/ui/cascader.tsx +754 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/chat.tsx +544 -0
- package/src/components/ui/checkbox.tsx +94 -0
- package/src/components/ui/clipboard.tsx +94 -0
- package/src/components/ui/collapsible.tsx +32 -0
- package/src/components/ui/color-picker.tsx +441 -0
- package/src/components/ui/combobox.tsx +400 -0
- package/src/components/ui/command.tsx +292 -0
- package/src/components/ui/comment-thread.tsx +923 -0
- package/src/components/ui/context-menu.tsx +331 -0
- package/src/components/ui/data-grid.tsx +1227 -0
- package/src/components/ui/data-table.tsx +993 -0
- package/src/components/ui/date-input.tsx +103 -0
- package/src/components/ui/dialog.tsx +159 -0
- package/src/components/ui/direction.tsx +32 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/dropdown-menu.tsx +329 -0
- package/src/components/ui/editable.tsx +90 -0
- package/src/components/ui/empty.tsx +86 -0
- package/src/components/ui/field.tsx +218 -0
- package/src/components/ui/file-upload.tsx +204 -0
- package/src/components/ui/floating-panel.tsx +199 -0
- package/src/components/ui/floating-toolbar.tsx +79 -0
- package/src/components/ui/format.tsx +22 -0
- package/src/components/ui/gantt.tsx +950 -0
- package/src/components/ui/hotkeys.tsx +340 -0
- package/src/components/ui/hover-card.tsx +83 -0
- package/src/components/ui/image-cropper.tsx +100 -0
- package/src/components/ui/input-group.tsx +133 -0
- package/src/components/ui/input-otp.tsx +82 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/item.tsx +174 -0
- package/src/components/ui/json-tree-view.tsx +37 -0
- package/src/components/ui/kanban.tsx +884 -0
- package/src/components/ui/kbd.tsx +20 -0
- package/src/components/ui/label.tsx +30 -0
- package/src/components/ui/live-region.tsx +54 -0
- package/src/components/ui/marker.tsx +66 -0
- package/src/components/ui/marquee.tsx +61 -0
- package/src/components/ui/menubar.tsx +456 -0
- package/src/components/ui/message.tsx +74 -0
- package/src/components/ui/navigation-menu.tsx +202 -0
- package/src/components/ui/node-graph.tsx +1723 -0
- package/src/components/ui/number-input.tsx +148 -0
- package/src/components/ui/pagination.tsx +88 -0
- package/src/components/ui/password-input.tsx +104 -0
- package/src/components/ui/popconfirm.tsx +149 -0
- package/src/components/ui/popover.tsx +126 -0
- package/src/components/ui/progress.tsx +114 -0
- package/src/components/ui/qr-code.tsx +51 -0
- package/src/components/ui/query-builder.tsx +938 -0
- package/src/components/ui/radio-group.tsx +108 -0
- package/src/components/ui/rating-group.tsx +91 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/rich-text-editor.tsx +845 -0
- package/src/components/ui/scheduler.tsx +1164 -0
- package/src/components/ui/scroll-area.tsx +82 -0
- package/src/components/ui/segment-group.tsx +95 -0
- package/src/components/ui/select.tsx +354 -0
- package/src/components/ui/separator.tsx +32 -0
- package/src/components/ui/sheet.tsx +130 -0
- package/src/components/ui/sidebar.tsx +665 -0
- package/src/components/ui/signature-pad.tsx +112 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +183 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/steps.tsx +152 -0
- package/src/components/ui/swap.tsx +33 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +100 -0
- package/src/components/ui/tags-input.tsx +153 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/timer.tsx +67 -0
- package/src/components/ui/toc.tsx +64 -0
- package/src/components/ui/toggle-group.tsx +89 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +113 -0
- package/src/components/ui/tour.tsx +168 -0
- package/src/components/ui/transfer-list.tsx +700 -0
- package/src/components/ui/tree-select.tsx +638 -0
- package/src/components/ui/tree-view.tsx +217 -0
- package/src/components/ui/virtual-list.tsx +343 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/controllable.ts +31 -0
- package/src/lib/data-table-adapter.ts +67 -0
- package/src/lib/drag-preview.ts +39 -0
- package/src/lib/form.ts +39 -0
- package/src/lib/history.ts +55 -0
- package/src/lib/row-layout.ts +81 -0
- package/src/lib/table.ts +483 -0
- package/src/lib/time.ts +37 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/arkcn.css +764 -0
- package/src/styles/index.css +3 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
BellIcon,
|
|
6
|
+
CheckIcon,
|
|
7
|
+
ChevronRightIcon,
|
|
8
|
+
ChevronsUpDownIcon,
|
|
9
|
+
MenuIcon,
|
|
10
|
+
MoonIcon,
|
|
11
|
+
SearchIcon,
|
|
12
|
+
SunIcon,
|
|
13
|
+
} from "lucide-react"
|
|
14
|
+
import { cn } from "@/lib/utils"
|
|
15
|
+
|
|
16
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
17
|
+
import { Badge } from "@/components/ui/badge"
|
|
18
|
+
import { Button } from "@/components/ui/button"
|
|
19
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
|
20
|
+
import { CommandDialog } from "@/components/ui/command"
|
|
21
|
+
import {
|
|
22
|
+
DropdownMenu,
|
|
23
|
+
DropdownMenuContent,
|
|
24
|
+
DropdownMenuGroup,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuLabel,
|
|
27
|
+
DropdownMenuSeparator,
|
|
28
|
+
DropdownMenuTrigger,
|
|
29
|
+
} from "@/components/ui/dropdown-menu"
|
|
30
|
+
import { Kbd } from "@/components/ui/kbd"
|
|
31
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
32
|
+
import { Separator } from "@/components/ui/separator"
|
|
33
|
+
import {
|
|
34
|
+
Sidebar,
|
|
35
|
+
SidebarContent,
|
|
36
|
+
SidebarFooter,
|
|
37
|
+
SidebarGroup,
|
|
38
|
+
SidebarGroupLabel,
|
|
39
|
+
SidebarHeader,
|
|
40
|
+
SidebarInset,
|
|
41
|
+
SidebarMenu,
|
|
42
|
+
SidebarMenuButton,
|
|
43
|
+
SidebarMenuItem,
|
|
44
|
+
SidebarMenuSub,
|
|
45
|
+
SidebarMenuSubButton,
|
|
46
|
+
SidebarMenuSubItem,
|
|
47
|
+
SidebarProvider,
|
|
48
|
+
SidebarRail,
|
|
49
|
+
SidebarTrigger,
|
|
50
|
+
useSidebar,
|
|
51
|
+
} from "@/components/ui/sidebar"
|
|
52
|
+
|
|
53
|
+
/* -------------------------------------------------------------------------- */
|
|
54
|
+
/* Root */
|
|
55
|
+
/* -------------------------------------------------------------------------- */
|
|
56
|
+
|
|
57
|
+
type SidebarVariant = "sidebar" | "floating" | "inset"
|
|
58
|
+
type SidebarCollapsible = "offcanvas" | "icon" | "none"
|
|
59
|
+
|
|
60
|
+
type AppShellContextValue = {
|
|
61
|
+
variant: SidebarVariant
|
|
62
|
+
setVariant: (variant: SidebarVariant) => void
|
|
63
|
+
collapsible: SidebarCollapsible
|
|
64
|
+
setCollapsible: (collapsible: SidebarCollapsible) => void
|
|
65
|
+
searchOpen: boolean
|
|
66
|
+
setSearchOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const AppShellContext = React.createContext<AppShellContextValue | null>(null)
|
|
70
|
+
|
|
71
|
+
function useAppShell() {
|
|
72
|
+
const ctx = React.useContext(AppShellContext)
|
|
73
|
+
if (!ctx) throw new Error("useAppShell must be used within <AppShell>")
|
|
74
|
+
return ctx
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Root of the shell. Owns the sidebar open state (via `SidebarProvider`), the
|
|
79
|
+
* sidebar variant and collapse mode, and the global search open state with its
|
|
80
|
+
* Cmd/Ctrl+K shortcut. Routing, theming, and data are the consumer's.
|
|
81
|
+
*/
|
|
82
|
+
function AppShell({
|
|
83
|
+
variant: variantProp,
|
|
84
|
+
defaultVariant = "inset",
|
|
85
|
+
onVariantChange,
|
|
86
|
+
collapsible: collapsibleProp,
|
|
87
|
+
defaultCollapsible = "icon",
|
|
88
|
+
onCollapsibleChange,
|
|
89
|
+
searchShortcut = true,
|
|
90
|
+
children,
|
|
91
|
+
...sidebarProviderProps
|
|
92
|
+
}: Omit<React.ComponentProps<typeof SidebarProvider>, "children"> & {
|
|
93
|
+
variant?: SidebarVariant
|
|
94
|
+
defaultVariant?: SidebarVariant
|
|
95
|
+
onVariantChange?: (variant: SidebarVariant) => void
|
|
96
|
+
collapsible?: SidebarCollapsible
|
|
97
|
+
defaultCollapsible?: SidebarCollapsible
|
|
98
|
+
onCollapsibleChange?: (collapsible: SidebarCollapsible) => void
|
|
99
|
+
/** Toggle the search dialog with Cmd/Ctrl+K. */
|
|
100
|
+
searchShortcut?: boolean
|
|
101
|
+
children?: React.ReactNode
|
|
102
|
+
}) {
|
|
103
|
+
const [variantState, setVariantState] = React.useState(defaultVariant)
|
|
104
|
+
const [collapsibleState, setCollapsibleState] = React.useState(defaultCollapsible)
|
|
105
|
+
const [searchOpen, setSearchOpen] = React.useState(false)
|
|
106
|
+
const variant = variantProp ?? variantState
|
|
107
|
+
const collapsible = collapsibleProp ?? collapsibleState
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!searchShortcut) return
|
|
111
|
+
const down = (event: KeyboardEvent) => {
|
|
112
|
+
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
setSearchOpen((open) => !open)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
document.addEventListener("keydown", down)
|
|
118
|
+
return () => document.removeEventListener("keydown", down)
|
|
119
|
+
}, [searchShortcut])
|
|
120
|
+
|
|
121
|
+
const ctx = React.useMemo<AppShellContextValue>(
|
|
122
|
+
() => ({
|
|
123
|
+
variant,
|
|
124
|
+
setVariant: (next) => {
|
|
125
|
+
if (variantProp === undefined) setVariantState(next)
|
|
126
|
+
onVariantChange?.(next)
|
|
127
|
+
},
|
|
128
|
+
collapsible,
|
|
129
|
+
setCollapsible: (next) => {
|
|
130
|
+
if (collapsibleProp === undefined) setCollapsibleState(next)
|
|
131
|
+
onCollapsibleChange?.(next)
|
|
132
|
+
},
|
|
133
|
+
searchOpen,
|
|
134
|
+
setSearchOpen,
|
|
135
|
+
}),
|
|
136
|
+
[variant, variantProp, onVariantChange, collapsible, collapsibleProp, onCollapsibleChange, searchOpen]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<AppShellContext.Provider value={ctx}>
|
|
141
|
+
<SidebarProvider {...sidebarProviderProps}>{children}</SidebarProvider>
|
|
142
|
+
</AppShellContext.Provider>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function AppShellSkipLink({
|
|
147
|
+
className,
|
|
148
|
+
href = "#content",
|
|
149
|
+
children = "Skip to main content",
|
|
150
|
+
...props
|
|
151
|
+
}: React.ComponentProps<"a">) {
|
|
152
|
+
return (
|
|
153
|
+
<a
|
|
154
|
+
data-slot="app-shell-skip-link"
|
|
155
|
+
href={href}
|
|
156
|
+
className={cn(
|
|
157
|
+
"fixed inset-s-44 z-999 -translate-y-52 rounded-md bg-primary px-4 py-2 text-sm font-medium whitespace-nowrap text-primary-foreground opacity-95 shadow-sm transition focus:translate-y-3 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
|
|
158
|
+
className
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</a>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* -------------------------------------------------------------------------- */
|
|
168
|
+
/* Sidebar */
|
|
169
|
+
/* -------------------------------------------------------------------------- */
|
|
170
|
+
|
|
171
|
+
function AppShellSidebar({ variant, collapsible, children, ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
172
|
+
const shell = useAppShell()
|
|
173
|
+
return (
|
|
174
|
+
<Sidebar
|
|
175
|
+
data-slot="app-shell-sidebar"
|
|
176
|
+
variant={variant ?? shell.variant}
|
|
177
|
+
collapsible={collapsible ?? shell.collapsible}
|
|
178
|
+
{...props}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
<SidebarRail />
|
|
182
|
+
</Sidebar>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Brand / team switcher trigger ------------------------------------------- */
|
|
187
|
+
|
|
188
|
+
function AppShellBrand({ className, ...props }: React.ComponentProps<typeof SidebarMenuButton>) {
|
|
189
|
+
return (
|
|
190
|
+
<SidebarMenu>
|
|
191
|
+
<SidebarMenuItem>
|
|
192
|
+
<SidebarMenuButton
|
|
193
|
+
data-slot="app-shell-brand"
|
|
194
|
+
size="lg"
|
|
195
|
+
className={cn("data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground", className)}
|
|
196
|
+
{...props}
|
|
197
|
+
/>
|
|
198
|
+
</SidebarMenuItem>
|
|
199
|
+
</SidebarMenu>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function AppShellBrandLogo({ className, ...props }: React.ComponentProps<"div">) {
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
data-slot="app-shell-brand-logo"
|
|
207
|
+
className={cn(
|
|
208
|
+
"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground [&_svg]:size-4",
|
|
209
|
+
className
|
|
210
|
+
)}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function AppShellBrandText({ className, ...props }: React.ComponentProps<"div">) {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
data-slot="app-shell-brand-text"
|
|
220
|
+
className={cn("grid flex-1 text-start text-sm/tight", className)}
|
|
221
|
+
{...props}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function AppShellBrandTitle({ className, ...props }: React.ComponentProps<"span">) {
|
|
227
|
+
return <span data-slot="app-shell-brand-title" className={cn("truncate font-semibold", className)} {...props} />
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function AppShellBrandDescription({ className, ...props }: React.ComponentProps<"span">) {
|
|
231
|
+
return <span data-slot="app-shell-brand-description" className={cn("truncate text-xs", className)} {...props} />
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function AppShellBrandChevron({ className, ...props }: React.ComponentProps<typeof ChevronsUpDownIcon>) {
|
|
235
|
+
return <ChevronsUpDownIcon data-slot="app-shell-brand-chevron" className={cn("ms-auto", className)} {...props} />
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Navigation --------------------------------------------------------------- */
|
|
239
|
+
|
|
240
|
+
function AppShellNav({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
|
|
241
|
+
return <SidebarGroup data-slot="app-shell-nav" {...props} />
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function AppShellNavLabel({ ...props }: React.ComponentProps<typeof SidebarGroupLabel>) {
|
|
245
|
+
return <SidebarGroupLabel data-slot="app-shell-nav-label" {...props} />
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function AppShellNavList({ ...props }: React.ComponentProps<typeof SidebarMenu>) {
|
|
249
|
+
return <SidebarMenu data-slot="app-shell-nav-list" {...props} />
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function AppShellNavItem({ ...props }: React.ComponentProps<typeof SidebarMenuItem>) {
|
|
253
|
+
return <SidebarMenuItem data-slot="app-shell-nav-item" {...props} />
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A top-level navigation link. Pass the link element as the child with
|
|
258
|
+
* `asChild` (default) so any router's link works. `active` marks the current
|
|
259
|
+
* route; `tooltip` shows when the sidebar is collapsed to icons.
|
|
260
|
+
*/
|
|
261
|
+
function AppShellNavLink({
|
|
262
|
+
active = false,
|
|
263
|
+
asChild = true,
|
|
264
|
+
onClick,
|
|
265
|
+
...props
|
|
266
|
+
}: Omit<React.ComponentProps<typeof SidebarMenuButton>, "isActive"> & {
|
|
267
|
+
active?: boolean
|
|
268
|
+
}) {
|
|
269
|
+
const { setOpenMobile } = useSidebar()
|
|
270
|
+
return (
|
|
271
|
+
<SidebarMenuButton
|
|
272
|
+
data-slot="app-shell-nav-link"
|
|
273
|
+
asChild={asChild}
|
|
274
|
+
isActive={active}
|
|
275
|
+
onClick={(event) => {
|
|
276
|
+
onClick?.(event)
|
|
277
|
+
setOpenMobile(false)
|
|
278
|
+
}}
|
|
279
|
+
{...props}
|
|
280
|
+
/>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function AppShellNavBadge({ className, ...props }: React.ComponentProps<typeof Badge>) {
|
|
285
|
+
return (
|
|
286
|
+
<Badge
|
|
287
|
+
data-slot="app-shell-nav-badge"
|
|
288
|
+
className={cn("ms-auto rounded-full px-1 py-0 text-xs", className)}
|
|
289
|
+
{...props}
|
|
290
|
+
/>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A navigation item with children. Expanded sidebar: a collapsible sub-list.
|
|
296
|
+
* Icon-collapsed sidebar on desktop: a flyout menu. Supply `trigger` for the
|
|
297
|
+
* button content and `menu` for the flyout items; `children` is the sub-list.
|
|
298
|
+
*/
|
|
299
|
+
function AppShellNavCollapsible({
|
|
300
|
+
active = false,
|
|
301
|
+
defaultOpen,
|
|
302
|
+
tooltip,
|
|
303
|
+
trigger,
|
|
304
|
+
menu,
|
|
305
|
+
children,
|
|
306
|
+
...props
|
|
307
|
+
}: React.ComponentProps<typeof SidebarMenuItem> & {
|
|
308
|
+
active?: boolean
|
|
309
|
+
defaultOpen?: boolean
|
|
310
|
+
tooltip?: string
|
|
311
|
+
trigger: React.ReactNode
|
|
312
|
+
/** Rendered inside the flyout when the sidebar is collapsed to icons. */
|
|
313
|
+
menu?: React.ReactNode
|
|
314
|
+
}) {
|
|
315
|
+
const { state, isMobile } = useSidebar()
|
|
316
|
+
|
|
317
|
+
if (state === "collapsed" && !isMobile) {
|
|
318
|
+
return (
|
|
319
|
+
<SidebarMenuItem data-slot="app-shell-nav-collapsible" {...props}>
|
|
320
|
+
<DropdownMenu positioning={{ placement: "right-start", gutter: 4 }}>
|
|
321
|
+
<DropdownMenuTrigger asChild>
|
|
322
|
+
<SidebarMenuButton tooltip={tooltip} isActive={active}>
|
|
323
|
+
{trigger}
|
|
324
|
+
<ChevronRightIcon className="ms-auto" />
|
|
325
|
+
</SidebarMenuButton>
|
|
326
|
+
</DropdownMenuTrigger>
|
|
327
|
+
<DropdownMenuContent data-slot="app-shell-nav-flyout">
|
|
328
|
+
<DropdownMenuGroup>
|
|
329
|
+
{tooltip && (
|
|
330
|
+
<>
|
|
331
|
+
<DropdownMenuLabel>{tooltip}</DropdownMenuLabel>
|
|
332
|
+
<DropdownMenuSeparator />
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
{menu}
|
|
336
|
+
</DropdownMenuGroup>
|
|
337
|
+
</DropdownMenuContent>
|
|
338
|
+
</DropdownMenu>
|
|
339
|
+
</SidebarMenuItem>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<Collapsible
|
|
345
|
+
defaultOpen={defaultOpen ?? active}
|
|
346
|
+
className="group/collapsible"
|
|
347
|
+
unmountOnExit={false}
|
|
348
|
+
lazyMount={false}
|
|
349
|
+
>
|
|
350
|
+
<SidebarMenuItem data-slot="app-shell-nav-collapsible" {...props}>
|
|
351
|
+
<CollapsibleTrigger asChild>
|
|
352
|
+
<SidebarMenuButton tooltip={tooltip} isActive={active}>
|
|
353
|
+
{trigger}
|
|
354
|
+
<ChevronRightIcon className="ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180" />
|
|
355
|
+
</SidebarMenuButton>
|
|
356
|
+
</CollapsibleTrigger>
|
|
357
|
+
<CollapsibleContent>
|
|
358
|
+
<SidebarMenuSub data-slot="app-shell-nav-sublist">{children}</SidebarMenuSub>
|
|
359
|
+
</CollapsibleContent>
|
|
360
|
+
</SidebarMenuItem>
|
|
361
|
+
</Collapsible>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function AppShellNavSubItem({ ...props }: React.ComponentProps<typeof SidebarMenuSubItem>) {
|
|
366
|
+
return <SidebarMenuSubItem data-slot="app-shell-nav-subitem" {...props} />
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function AppShellNavSubLink({
|
|
370
|
+
active = false,
|
|
371
|
+
asChild = true,
|
|
372
|
+
onClick,
|
|
373
|
+
...props
|
|
374
|
+
}: Omit<React.ComponentProps<typeof SidebarMenuSubButton>, "isActive"> & { active?: boolean }) {
|
|
375
|
+
const { setOpenMobile } = useSidebar()
|
|
376
|
+
return (
|
|
377
|
+
<SidebarMenuSubButton
|
|
378
|
+
data-slot="app-shell-nav-sublink"
|
|
379
|
+
asChild={asChild}
|
|
380
|
+
isActive={active}
|
|
381
|
+
onClick={(event) => {
|
|
382
|
+
onClick?.(event)
|
|
383
|
+
setOpenMobile(false)
|
|
384
|
+
}}
|
|
385
|
+
{...props}
|
|
386
|
+
/>
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** A flyout menu entry for the icon-collapsed state. */
|
|
391
|
+
function AppShellNavMenuLink({
|
|
392
|
+
active = false,
|
|
393
|
+
className,
|
|
394
|
+
asChild = true,
|
|
395
|
+
...props
|
|
396
|
+
}: React.ComponentProps<typeof DropdownMenuItem> & { active?: boolean }) {
|
|
397
|
+
return (
|
|
398
|
+
<DropdownMenuItem
|
|
399
|
+
data-slot="app-shell-nav-menu-link"
|
|
400
|
+
asChild={asChild}
|
|
401
|
+
className={cn(active && "bg-secondary", className)}
|
|
402
|
+
{...props}
|
|
403
|
+
/>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* User footer trigger ------------------------------------------------------ */
|
|
408
|
+
|
|
409
|
+
function AppShellUser({ className, ...props }: React.ComponentProps<typeof SidebarMenuButton>) {
|
|
410
|
+
return (
|
|
411
|
+
<SidebarMenu>
|
|
412
|
+
<SidebarMenuItem>
|
|
413
|
+
<SidebarMenuButton
|
|
414
|
+
data-slot="app-shell-user"
|
|
415
|
+
size="lg"
|
|
416
|
+
className={cn("data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground", className)}
|
|
417
|
+
{...props}
|
|
418
|
+
/>
|
|
419
|
+
</SidebarMenuItem>
|
|
420
|
+
</SidebarMenu>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function AppShellUserAvatar({
|
|
425
|
+
src,
|
|
426
|
+
alt,
|
|
427
|
+
fallback,
|
|
428
|
+
className,
|
|
429
|
+
...props
|
|
430
|
+
}: React.ComponentProps<typeof Avatar> & { src?: string; alt?: string; fallback: React.ReactNode }) {
|
|
431
|
+
return (
|
|
432
|
+
<Avatar data-slot="app-shell-user-avatar" className={cn("rounded-lg", className)} {...props}>
|
|
433
|
+
{src && <AvatarImage src={src} alt={alt} />}
|
|
434
|
+
<AvatarFallback className="rounded-lg">{fallback}</AvatarFallback>
|
|
435
|
+
</Avatar>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function AppShellUserText({ className, ...props }: React.ComponentProps<"div">) {
|
|
440
|
+
return (
|
|
441
|
+
<div data-slot="app-shell-user-text" className={cn("grid flex-1 text-start text-sm/tight", className)} {...props} />
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function AppShellUserName({ className, ...props }: React.ComponentProps<"span">) {
|
|
446
|
+
return <span data-slot="app-shell-user-name" className={cn("truncate font-semibold", className)} {...props} />
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function AppShellUserEmail({ className, ...props }: React.ComponentProps<"span">) {
|
|
450
|
+
return <span data-slot="app-shell-user-email" className={cn("truncate text-xs", className)} {...props} />
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* -------------------------------------------------------------------------- */
|
|
454
|
+
/* Content area: header, main */
|
|
455
|
+
/* -------------------------------------------------------------------------- */
|
|
456
|
+
|
|
457
|
+
function AppShellContent({ className, ...props }: React.ComponentProps<typeof SidebarInset>) {
|
|
458
|
+
return (
|
|
459
|
+
<SidebarInset
|
|
460
|
+
data-slot="app-shell-content"
|
|
461
|
+
id="content"
|
|
462
|
+
className={cn(
|
|
463
|
+
"@container/content",
|
|
464
|
+
"has-data-[layout=fixed]:h-svh",
|
|
465
|
+
"peer-data-[variant=inset]:has-data-[layout=fixed]:h-[calc(100svh-(var(--spacing)*4))]",
|
|
466
|
+
className
|
|
467
|
+
)}
|
|
468
|
+
{...props}
|
|
469
|
+
/>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function AppShellHeader({
|
|
474
|
+
className,
|
|
475
|
+
fixed = false,
|
|
476
|
+
children,
|
|
477
|
+
...props
|
|
478
|
+
}: React.ComponentProps<"header"> & { fixed?: boolean }) {
|
|
479
|
+
const [scrolled, setScrolled] = React.useState(false)
|
|
480
|
+
React.useEffect(() => {
|
|
481
|
+
if (!fixed) return
|
|
482
|
+
const onScroll = () => setScrolled((document.body.scrollTop || document.documentElement.scrollTop) > 10)
|
|
483
|
+
onScroll()
|
|
484
|
+
document.addEventListener("scroll", onScroll, { passive: true })
|
|
485
|
+
return () => document.removeEventListener("scroll", onScroll)
|
|
486
|
+
}, [fixed])
|
|
487
|
+
return (
|
|
488
|
+
<header
|
|
489
|
+
data-slot="app-shell-header"
|
|
490
|
+
data-fixed={fixed ? "" : undefined}
|
|
491
|
+
data-scrolled={scrolled ? "" : undefined}
|
|
492
|
+
className={cn(
|
|
493
|
+
// Solid background; the top corners follow the inset card's radius so the
|
|
494
|
+
// header never squares off the rounded content area.
|
|
495
|
+
"z-50 h-16 rounded-t-[inherit] bg-background",
|
|
496
|
+
fixed && "peer/header sticky top-0 w-[inherit]",
|
|
497
|
+
fixed && scrolled ? "shadow-sm" : "shadow-none",
|
|
498
|
+
className
|
|
499
|
+
)}
|
|
500
|
+
{...props}
|
|
501
|
+
>
|
|
502
|
+
<div
|
|
503
|
+
className={cn(
|
|
504
|
+
"relative flex h-full items-center gap-3 p-4 sm:gap-4",
|
|
505
|
+
fixed &&
|
|
506
|
+
scrolled &&
|
|
507
|
+
"after:absolute after:inset-0 after:-z-10 after:rounded-[inherit] after:bg-background/60 after:backdrop-blur-lg"
|
|
508
|
+
)}
|
|
509
|
+
>
|
|
510
|
+
<SidebarTrigger variant="outline" className="max-md:scale-125" />
|
|
511
|
+
<Separator orientation="vertical" className="h-6" />
|
|
512
|
+
{children}
|
|
513
|
+
</div>
|
|
514
|
+
</header>
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function AppShellHeaderActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
519
|
+
return (
|
|
520
|
+
<div
|
|
521
|
+
data-slot="app-shell-header-actions"
|
|
522
|
+
className={cn("ms-auto flex items-center gap-3 sm:gap-4", className)}
|
|
523
|
+
{...props}
|
|
524
|
+
/>
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function AppShellMain({
|
|
529
|
+
className,
|
|
530
|
+
fixed = false,
|
|
531
|
+
fluid = false,
|
|
532
|
+
...props
|
|
533
|
+
}: React.ComponentProps<"main"> & { fixed?: boolean; fluid?: boolean }) {
|
|
534
|
+
return (
|
|
535
|
+
<main
|
|
536
|
+
data-slot="app-shell-main"
|
|
537
|
+
data-layout={fixed ? "fixed" : "auto"}
|
|
538
|
+
className={cn(
|
|
539
|
+
"px-4 py-6",
|
|
540
|
+
fixed && "flex grow flex-col overflow-hidden",
|
|
541
|
+
!fluid && "@7xl/content:mx-auto @7xl/content:w-full @7xl/content:max-w-7xl",
|
|
542
|
+
className
|
|
543
|
+
)}
|
|
544
|
+
{...props}
|
|
545
|
+
/>
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/* Top nav (secondary, in the header) --------------------------------------- */
|
|
550
|
+
|
|
551
|
+
function AppShellTopNav({ className, ...props }: React.ComponentProps<"nav">) {
|
|
552
|
+
return (
|
|
553
|
+
<nav
|
|
554
|
+
data-slot="app-shell-top-nav"
|
|
555
|
+
className={cn("hidden items-center gap-4 lg:flex xl:gap-6", className)}
|
|
556
|
+
{...props}
|
|
557
|
+
/>
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function AppShellTopNavLink({ active = false, className, ...props }: React.ComponentProps<"a"> & { active?: boolean }) {
|
|
562
|
+
return (
|
|
563
|
+
<a
|
|
564
|
+
data-slot="app-shell-top-nav-link"
|
|
565
|
+
data-active={active ? "" : undefined}
|
|
566
|
+
className={cn(
|
|
567
|
+
"text-sm font-medium text-muted-foreground transition-colors hover:text-primary data-active:text-foreground",
|
|
568
|
+
className
|
|
569
|
+
)}
|
|
570
|
+
{...props}
|
|
571
|
+
/>
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** The small-screen counterpart of `AppShellTopNav`: the same links in a menu. */
|
|
576
|
+
function AppShellTopNavMenu({
|
|
577
|
+
className,
|
|
578
|
+
children,
|
|
579
|
+
...props
|
|
580
|
+
}: React.ComponentProps<typeof DropdownMenu> & { className?: string }) {
|
|
581
|
+
return (
|
|
582
|
+
<DropdownMenu positioning={{ placement: "bottom-start" }} {...props}>
|
|
583
|
+
<DropdownMenuTrigger asChild>
|
|
584
|
+
<Button
|
|
585
|
+
data-slot="app-shell-top-nav-menu-trigger"
|
|
586
|
+
size="icon"
|
|
587
|
+
variant="outline"
|
|
588
|
+
className={cn("md:size-7 lg:hidden", className)}
|
|
589
|
+
>
|
|
590
|
+
<MenuIcon />
|
|
591
|
+
<span className="sr-only">Toggle navigation menu</span>
|
|
592
|
+
</Button>
|
|
593
|
+
</DropdownMenuTrigger>
|
|
594
|
+
<DropdownMenuContent>{children}</DropdownMenuContent>
|
|
595
|
+
</DropdownMenu>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/* -------------------------------------------------------------------------- */
|
|
600
|
+
/* Search and command menu */
|
|
601
|
+
/* -------------------------------------------------------------------------- */
|
|
602
|
+
|
|
603
|
+
function AppShellSearch({
|
|
604
|
+
className,
|
|
605
|
+
placeholder = "Search",
|
|
606
|
+
onClick,
|
|
607
|
+
asChild,
|
|
608
|
+
children,
|
|
609
|
+
...props
|
|
610
|
+
}: React.ComponentProps<typeof Button> & { placeholder?: string }) {
|
|
611
|
+
const shell = useAppShell()
|
|
612
|
+
return (
|
|
613
|
+
<Button
|
|
614
|
+
data-slot="app-shell-search"
|
|
615
|
+
variant="outline"
|
|
616
|
+
asChild={asChild}
|
|
617
|
+
className={cn(
|
|
618
|
+
"group relative h-8 w-full flex-1 justify-start rounded-md bg-muted/25 text-sm font-normal text-muted-foreground shadow-none hover:bg-accent sm:w-40 sm:pe-12 md:flex-none lg:w-52 xl:w-64",
|
|
619
|
+
className
|
|
620
|
+
)}
|
|
621
|
+
aria-keyshortcuts="Meta+K Control+K"
|
|
622
|
+
onClick={(event) => {
|
|
623
|
+
onClick?.(event)
|
|
624
|
+
if (!event.defaultPrevented) shell.setSearchOpen(true)
|
|
625
|
+
}}
|
|
626
|
+
{...props}
|
|
627
|
+
>
|
|
628
|
+
{asChild
|
|
629
|
+
? children
|
|
630
|
+
: (children ?? (
|
|
631
|
+
<>
|
|
632
|
+
<SearchIcon aria-hidden className="absolute inset-s-1.5 top-1/2 size-4 -translate-y-1/2" />
|
|
633
|
+
<span className="ms-4">{placeholder}</span>
|
|
634
|
+
<Kbd className="pointer-events-none absolute inset-e-1.5 top-1.5 hidden sm:flex">⌘K</Kbd>
|
|
635
|
+
</>
|
|
636
|
+
))}
|
|
637
|
+
</Button>
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** A `CommandDialog` bound to the shell's search state. */
|
|
642
|
+
function AppShellCommandDialog({
|
|
643
|
+
...props
|
|
644
|
+
}: Omit<React.ComponentProps<typeof CommandDialog>, "open" | "onOpenChange">) {
|
|
645
|
+
const shell = useAppShell()
|
|
646
|
+
return <CommandDialog open={shell.searchOpen} onOpenChange={({ open }) => shell.setSearchOpen(open)} {...props} />
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* -------------------------------------------------------------------------- */
|
|
650
|
+
/* Notifications */
|
|
651
|
+
/* -------------------------------------------------------------------------- */
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* A header notifications popover. Data-agnostic: the consumer renders the
|
|
655
|
+
* items and owns read state; pass `count` to the trigger for the unread badge.
|
|
656
|
+
*/
|
|
657
|
+
function AppShellNotifications({ positioning, ...props }: React.ComponentProps<typeof Popover>) {
|
|
658
|
+
return (
|
|
659
|
+
<Popover
|
|
660
|
+
data-slot="app-shell-notifications"
|
|
661
|
+
positioning={{ placement: "bottom-end", gutter: 8, ...positioning }}
|
|
662
|
+
{...props}
|
|
663
|
+
/>
|
|
664
|
+
)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function AppShellNotificationsTrigger({
|
|
668
|
+
count = 0,
|
|
669
|
+
max = 99,
|
|
670
|
+
className,
|
|
671
|
+
children,
|
|
672
|
+
asChild,
|
|
673
|
+
...props
|
|
674
|
+
}: React.ComponentProps<typeof Button> & {
|
|
675
|
+
/** Unread count shown as a badge; hidden when 0. */
|
|
676
|
+
count?: number
|
|
677
|
+
max?: number
|
|
678
|
+
}) {
|
|
679
|
+
const label = count > max ? `${max}+` : String(count)
|
|
680
|
+
return (
|
|
681
|
+
<PopoverTrigger asChild>
|
|
682
|
+
<Button
|
|
683
|
+
data-slot="app-shell-notifications-trigger"
|
|
684
|
+
data-unread={count > 0 ? "" : undefined}
|
|
685
|
+
variant="ghost"
|
|
686
|
+
size="icon-lg"
|
|
687
|
+
asChild={asChild}
|
|
688
|
+
className={cn("relative scale-95 rounded-full", className)}
|
|
689
|
+
aria-label={count > 0 ? `Notifications, ${label} unread` : "Notifications"}
|
|
690
|
+
{...props}
|
|
691
|
+
>
|
|
692
|
+
{asChild ? children : (children ?? <BellIcon className="size-[1.2rem]" />)}
|
|
693
|
+
{!asChild && count > 0 && (
|
|
694
|
+
<span
|
|
695
|
+
data-slot="app-shell-notifications-badge"
|
|
696
|
+
aria-hidden
|
|
697
|
+
className="absolute inset-e-0.5 top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] leading-none font-medium text-white ring-2 ring-background"
|
|
698
|
+
>
|
|
699
|
+
{label}
|
|
700
|
+
</span>
|
|
701
|
+
)}
|
|
702
|
+
</Button>
|
|
703
|
+
</PopoverTrigger>
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function AppShellNotificationsContent({ className, ...props }: React.ComponentProps<typeof PopoverContent>) {
|
|
708
|
+
return (
|
|
709
|
+
<PopoverContent
|
|
710
|
+
data-slot="app-shell-notifications-content"
|
|
711
|
+
className={cn("w-80 gap-0 p-0 sm:w-96", className)}
|
|
712
|
+
{...props}
|
|
713
|
+
/>
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function AppShellNotificationsHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
718
|
+
return (
|
|
719
|
+
<div
|
|
720
|
+
data-slot="app-shell-notifications-header"
|
|
721
|
+
className={cn("flex items-center justify-between gap-2 border-b px-3.5 py-2.5", className)}
|
|
722
|
+
{...props}
|
|
723
|
+
/>
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function AppShellNotificationsTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
|
728
|
+
return <h3 data-slot="app-shell-notifications-title" className={cn("text-sm font-semibold", className)} {...props} />
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** A small text action for the header or footer, e.g. "Mark all as read". */
|
|
732
|
+
function AppShellNotificationsActionTrigger({
|
|
733
|
+
className,
|
|
734
|
+
variant = "link",
|
|
735
|
+
size = "sm",
|
|
736
|
+
...props
|
|
737
|
+
}: React.ComponentProps<typeof Button>) {
|
|
738
|
+
return (
|
|
739
|
+
<Button
|
|
740
|
+
data-slot="app-shell-notifications-action-trigger"
|
|
741
|
+
variant={variant}
|
|
742
|
+
size={size}
|
|
743
|
+
className={cn("h-auto p-0 text-xs text-muted-foreground hover:text-foreground", className)}
|
|
744
|
+
{...props}
|
|
745
|
+
/>
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function AppShellNotificationsList({ className, ...props }: React.ComponentProps<"ul">) {
|
|
750
|
+
return (
|
|
751
|
+
<ul
|
|
752
|
+
data-slot="app-shell-notifications-list"
|
|
753
|
+
className={cn("no-scrollbar flex max-h-96 flex-col gap-0.5 overflow-y-auto p-1", className)}
|
|
754
|
+
{...props}
|
|
755
|
+
/>
|
|
756
|
+
)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function AppShellNotificationItem({
|
|
760
|
+
unread,
|
|
761
|
+
asChild,
|
|
762
|
+
className,
|
|
763
|
+
children,
|
|
764
|
+
...props
|
|
765
|
+
}: React.ComponentProps<"li"> & {
|
|
766
|
+
unread?: boolean
|
|
767
|
+
/** Render the item's child (e.g. an anchor or button) as the interactive surface. */
|
|
768
|
+
asChild?: boolean
|
|
769
|
+
}) {
|
|
770
|
+
const itemClassName = cn(
|
|
771
|
+
"group/notification relative flex w-full cursor-default gap-3 rounded-md px-2.5 py-2 text-start text-sm outline-hidden transition-colors select-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-unread:bg-primary/5 dark:data-unread:bg-primary/10",
|
|
772
|
+
className
|
|
773
|
+
)
|
|
774
|
+
const inner =
|
|
775
|
+
asChild && React.isValidElement(children)
|
|
776
|
+
? React.cloneElement(children as React.ReactElement<Record<string, unknown>>, {
|
|
777
|
+
"data-slot": "app-shell-notification-item-surface",
|
|
778
|
+
"data-unread": unread ? "" : undefined,
|
|
779
|
+
className: cn(itemClassName, (children as React.ReactElement<{ className?: string }>).props.className),
|
|
780
|
+
})
|
|
781
|
+
: children
|
|
782
|
+
return (
|
|
783
|
+
<li
|
|
784
|
+
data-slot="app-shell-notification-item"
|
|
785
|
+
data-unread={unread ? "" : undefined}
|
|
786
|
+
className={asChild ? "flex" : itemClassName}
|
|
787
|
+
{...props}
|
|
788
|
+
>
|
|
789
|
+
{inner}
|
|
790
|
+
</li>
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function AppShellNotificationItemIcon({ className, ...props }: React.ComponentProps<"div">) {
|
|
795
|
+
return (
|
|
796
|
+
<div
|
|
797
|
+
data-slot="app-shell-notification-item-icon"
|
|
798
|
+
className={cn(
|
|
799
|
+
"flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-4",
|
|
800
|
+
className
|
|
801
|
+
)}
|
|
802
|
+
{...props}
|
|
803
|
+
/>
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function AppShellNotificationItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
808
|
+
return (
|
|
809
|
+
<div
|
|
810
|
+
data-slot="app-shell-notification-item-content"
|
|
811
|
+
className={cn("flex min-w-0 flex-1 flex-col gap-0.5", className)}
|
|
812
|
+
{...props}
|
|
813
|
+
/>
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function AppShellNotificationItemTitle({ className, ...props }: React.ComponentProps<"p">) {
|
|
818
|
+
return (
|
|
819
|
+
<p
|
|
820
|
+
data-slot="app-shell-notification-item-title"
|
|
821
|
+
className={cn("truncate leading-tight font-medium group-data-unread/notification:text-foreground", className)}
|
|
822
|
+
{...props}
|
|
823
|
+
/>
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function AppShellNotificationItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
828
|
+
return (
|
|
829
|
+
<p
|
|
830
|
+
data-slot="app-shell-notification-item-description"
|
|
831
|
+
className={cn("line-clamp-2 text-xs text-muted-foreground", className)}
|
|
832
|
+
{...props}
|
|
833
|
+
/>
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function AppShellNotificationItemTime({ className, ...props }: React.ComponentProps<"time">) {
|
|
838
|
+
return (
|
|
839
|
+
<time
|
|
840
|
+
data-slot="app-shell-notification-item-time"
|
|
841
|
+
className={cn("text-[11px] text-muted-foreground", className)}
|
|
842
|
+
{...props}
|
|
843
|
+
/>
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** The unread dot; renders only inside an unread item. */
|
|
848
|
+
function AppShellNotificationItemIndicator({ className, ...props }: React.ComponentProps<"span">) {
|
|
849
|
+
return (
|
|
850
|
+
<span
|
|
851
|
+
data-slot="app-shell-notification-item-indicator"
|
|
852
|
+
aria-hidden
|
|
853
|
+
className={cn(
|
|
854
|
+
"mt-1.5 hidden size-2 shrink-0 rounded-full bg-primary group-data-unread/notification:block",
|
|
855
|
+
className
|
|
856
|
+
)}
|
|
857
|
+
{...props}
|
|
858
|
+
/>
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function AppShellNotificationsEmpty({ className, ...props }: React.ComponentProps<"div">) {
|
|
863
|
+
return (
|
|
864
|
+
<div
|
|
865
|
+
data-slot="app-shell-notifications-empty"
|
|
866
|
+
className={cn(
|
|
867
|
+
"flex flex-col items-center justify-center gap-1 px-4 py-10 text-center text-sm text-muted-foreground [&_svg]:mb-1 [&_svg]:size-6",
|
|
868
|
+
className
|
|
869
|
+
)}
|
|
870
|
+
{...props}
|
|
871
|
+
/>
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function AppShellNotificationsFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
876
|
+
return (
|
|
877
|
+
<div
|
|
878
|
+
data-slot="app-shell-notifications-footer"
|
|
879
|
+
className={cn("flex items-center justify-center border-t px-3.5 py-2", className)}
|
|
880
|
+
{...props}
|
|
881
|
+
/>
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/* -------------------------------------------------------------------------- */
|
|
886
|
+
/* Theme toggle */
|
|
887
|
+
/* -------------------------------------------------------------------------- */
|
|
888
|
+
|
|
889
|
+
type Theme = "light" | "dark" | "system"
|
|
890
|
+
|
|
891
|
+
function AppShellThemeToggle({
|
|
892
|
+
theme,
|
|
893
|
+
onThemeChange,
|
|
894
|
+
className,
|
|
895
|
+
...props
|
|
896
|
+
}: Omit<React.ComponentProps<typeof Button>, "onChange"> & {
|
|
897
|
+
theme: Theme
|
|
898
|
+
onThemeChange: (theme: Theme) => void
|
|
899
|
+
}) {
|
|
900
|
+
return (
|
|
901
|
+
<DropdownMenu positioning={{ placement: "bottom-end" }}>
|
|
902
|
+
<DropdownMenuTrigger asChild>
|
|
903
|
+
<Button
|
|
904
|
+
data-slot="app-shell-theme-toggle"
|
|
905
|
+
variant="ghost"
|
|
906
|
+
size="icon-lg"
|
|
907
|
+
className={cn("scale-95 rounded-full", className)}
|
|
908
|
+
{...props}
|
|
909
|
+
>
|
|
910
|
+
<SunIcon className="size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
911
|
+
<MoonIcon className="absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
912
|
+
<span className="sr-only">Toggle theme</span>
|
|
913
|
+
</Button>
|
|
914
|
+
</DropdownMenuTrigger>
|
|
915
|
+
<DropdownMenuContent>
|
|
916
|
+
{(["light", "dark", "system"] as const).map((option) => (
|
|
917
|
+
<DropdownMenuItem key={option} value={option} onSelect={() => onThemeChange(option)}>
|
|
918
|
+
<span className="capitalize">{option}</span>
|
|
919
|
+
<CheckIcon className={cn("ms-auto size-3.5", theme !== option && "invisible")} />
|
|
920
|
+
</DropdownMenuItem>
|
|
921
|
+
))}
|
|
922
|
+
</DropdownMenuContent>
|
|
923
|
+
</DropdownMenu>
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export {
|
|
928
|
+
AppShell,
|
|
929
|
+
AppShellBrand,
|
|
930
|
+
AppShellBrandChevron,
|
|
931
|
+
AppShellBrandDescription,
|
|
932
|
+
AppShellBrandLogo,
|
|
933
|
+
AppShellBrandText,
|
|
934
|
+
AppShellBrandTitle,
|
|
935
|
+
AppShellCommandDialog,
|
|
936
|
+
AppShellContent,
|
|
937
|
+
AppShellHeader,
|
|
938
|
+
AppShellHeaderActions,
|
|
939
|
+
AppShellMain,
|
|
940
|
+
AppShellNav,
|
|
941
|
+
AppShellNavBadge,
|
|
942
|
+
AppShellNavCollapsible,
|
|
943
|
+
AppShellNavItem,
|
|
944
|
+
AppShellNavLabel,
|
|
945
|
+
AppShellNavLink,
|
|
946
|
+
AppShellNavList,
|
|
947
|
+
AppShellNavMenuLink,
|
|
948
|
+
AppShellNavSubItem,
|
|
949
|
+
AppShellNavSubLink,
|
|
950
|
+
AppShellNotificationItem,
|
|
951
|
+
AppShellNotificationItemContent,
|
|
952
|
+
AppShellNotificationItemDescription,
|
|
953
|
+
AppShellNotificationItemIcon,
|
|
954
|
+
AppShellNotificationItemIndicator,
|
|
955
|
+
AppShellNotificationItemTime,
|
|
956
|
+
AppShellNotificationItemTitle,
|
|
957
|
+
AppShellNotifications,
|
|
958
|
+
AppShellNotificationsActionTrigger,
|
|
959
|
+
AppShellNotificationsContent,
|
|
960
|
+
AppShellNotificationsEmpty,
|
|
961
|
+
AppShellNotificationsFooter,
|
|
962
|
+
AppShellNotificationsHeader,
|
|
963
|
+
AppShellNotificationsList,
|
|
964
|
+
AppShellNotificationsTitle,
|
|
965
|
+
AppShellNotificationsTrigger,
|
|
966
|
+
AppShellSearch,
|
|
967
|
+
AppShellSidebar,
|
|
968
|
+
AppShellSkipLink,
|
|
969
|
+
AppShellThemeToggle,
|
|
970
|
+
AppShellTopNav,
|
|
971
|
+
AppShellTopNavLink,
|
|
972
|
+
AppShellTopNavMenu,
|
|
973
|
+
AppShellUser,
|
|
974
|
+
AppShellUserAvatar,
|
|
975
|
+
AppShellUserEmail,
|
|
976
|
+
AppShellUserName,
|
|
977
|
+
AppShellUserText,
|
|
978
|
+
SidebarContent as AppShellSidebarContent,
|
|
979
|
+
SidebarFooter as AppShellSidebarFooter,
|
|
980
|
+
SidebarHeader as AppShellSidebarHeader,
|
|
981
|
+
useAppShell,
|
|
982
|
+
type SidebarCollapsible,
|
|
983
|
+
type SidebarVariant,
|
|
984
|
+
type Theme,
|
|
985
|
+
}
|