@mostrom/app-shell 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/.claude/ralph-loop.local.md +9 -0
- package/README.md +172 -0
- package/bin/init.js +269 -0
- package/bun.lock +401 -0
- package/components.json +28 -0
- package/package.json +74 -0
- package/scripts/publish-npm.sh +202 -0
- package/src/AppShell.tsx +847 -0
- package/src/components/PageHeader.tsx +160 -0
- package/src/components/data-table/README.md +447 -0
- package/src/components/data-table/data-table-preferences.tsx +184 -0
- package/src/components/data-table/data-table-toolbar.tsx +118 -0
- package/src/components/data-table/data-table.tsx +37 -0
- package/src/components/data-table/index.ts +32 -0
- package/src/components/global-header/AllServicesButton.tsx +127 -0
- package/src/components/global-header/CategoriesButton.tsx +120 -0
- package/src/components/global-header/GlobalHeader.tsx +59 -0
- package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
- package/src/components/global-header/HeaderUtilities.tsx +243 -0
- package/src/components/global-header/ServicesMenu.tsx +246 -0
- package/src/components/layout/AppBreadcrumb.tsx +70 -0
- package/src/components/layout/AppFlashbar.tsx +95 -0
- package/src/components/layout/AppLayout.tsx +271 -0
- package/src/components/layout/AppNavigation.tsx +313 -0
- package/src/components/layout/AppSidebar.tsx +229 -0
- package/src/components/patterns/index.ts +14 -0
- package/src/components/patterns/p-alert-5.tsx +19 -0
- package/src/components/patterns/p-autocomplete-5.tsx +89 -0
- package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
- package/src/components/patterns/p-button-42.tsx +37 -0
- package/src/components/patterns/p-button-51.tsx +14 -0
- package/src/components/patterns/p-button-6.tsx +5 -0
- package/src/components/patterns/p-calendar-1.tsx +18 -0
- package/src/components/patterns/p-card-1.tsx +33 -0
- package/src/components/patterns/p-card-2.tsx +26 -0
- package/src/components/patterns/p-card-5.tsx +31 -0
- package/src/components/patterns/p-collapsible-7.tsx +121 -0
- package/src/components/patterns/p-command-6.tsx +113 -0
- package/src/components/patterns/p-dialog-1.tsx +56 -0
- package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
- package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
- package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
- package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
- package/src/components/patterns/p-empty-2.tsx +34 -0
- package/src/components/patterns/p-file-upload-1.tsx +72 -0
- package/src/components/patterns/p-filters-1.tsx +666 -0
- package/src/components/patterns/p-frame-2.tsx +26 -0
- package/src/components/patterns/p-tabs-2.tsx +129 -0
- package/src/components/reui/alert.tsx +92 -0
- package/src/components/reui/autocomplete.tsx +343 -0
- package/src/components/reui/badge.tsx +87 -0
- package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
- package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
- package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
- package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
- package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
- package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
- package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
- package/src/components/reui/data-grid/data-grid.tsx +209 -0
- package/src/components/reui/date-selector.tsx +1330 -0
- package/src/components/reui/filters.tsx +1869 -0
- package/src/components/reui/frame.tsx +134 -0
- package/src/components/reui/index.ts +17 -0
- package/src/components/reui/timeline.tsx +219 -0
- package/src/components/search/Autocomplete.tsx +183 -0
- package/src/components/search/AutocompleteClient.tsx +293 -0
- package/src/components/search/GlobalSearch.tsx +187 -0
- package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
- package/src/components/section-drawer/index.ts +19 -0
- package/src/components/section-drawer/section-drawer.css +665 -0
- package/src/components/section-drawer/section-drawer.tsx +467 -0
- package/src/components/sectioned-list-board/README.md +78 -0
- package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
- package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
- package/src/components/sectioned-list-board/index.ts +19 -0
- package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
- package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
- package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
- package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
- package/src/components/sectioned-list-board/types.ts +216 -0
- package/src/components/sectioned-list-table/README.md +80 -0
- package/src/components/sectioned-list-table/index.ts +14 -0
- package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
- package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
- package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
- package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
- package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
- package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
- package/src/components/sectioned-list-table/types.ts +120 -0
- package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
- package/src/components/ui/actions-dropdown.tsx +109 -0
- package/src/components/ui/assignee-selector.tsx +209 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/chart.tsx +376 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +182 -0
- package/src/components/ui/context-menu.tsx +250 -0
- package/src/components/ui/create-button-group.tsx +128 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/drawer.tsx +133 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/index.ts +37 -0
- package/src/components/ui/input-group.tsx +168 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/page-header.tsx +80 -0
- package/src/components/ui/popover.tsx +87 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +141 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +38 -0
- package/src/components/ui/switch.tsx +33 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-copy-to-clipboard.ts +37 -0
- package/src/hooks/use-file-upload.ts +415 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +95 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles.css +1859 -0
- package/src/urls.ts +83 -0
- package/src/vite.d.ts +22 -0
- package/src/vite.js +241 -0
- package/tsconfig.base.json +18 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { ChevronDownIcon } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu"
|
|
14
|
+
|
|
15
|
+
export interface ActionsDropdownItem {
|
|
16
|
+
/** Unique identifier for the item */
|
|
17
|
+
id: string
|
|
18
|
+
/** Display text for the item */
|
|
19
|
+
label: string
|
|
20
|
+
/** Optional icon to display before the label */
|
|
21
|
+
icon?: React.ReactNode
|
|
22
|
+
/** Click handler for the item */
|
|
23
|
+
onClick?: () => void
|
|
24
|
+
/** Whether the item is disabled */
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
/** Visual variant - use "destructive" for delete actions */
|
|
27
|
+
variant?: "default" | "destructive"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ActionsDropdownProps {
|
|
31
|
+
/** The items to display in the dropdown */
|
|
32
|
+
items: ActionsDropdownItem[]
|
|
33
|
+
/** The label for the dropdown trigger button */
|
|
34
|
+
children?: React.ReactNode
|
|
35
|
+
/** Button variant */
|
|
36
|
+
variant?: "default" | "outline" | "secondary" | "ghost" | "primary"
|
|
37
|
+
/** Alignment of the dropdown content */
|
|
38
|
+
align?: "start" | "center" | "end"
|
|
39
|
+
/** Additional class name for the trigger button */
|
|
40
|
+
className?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* ActionsDropdown - A dropdown menu for action buttons.
|
|
45
|
+
*
|
|
46
|
+
* This component replaces Cloudscape's ButtonDropdown with our design system.
|
|
47
|
+
* Use it for action menus like "Actions", "More options", etc.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <ActionsDropdown
|
|
52
|
+
* items={[
|
|
53
|
+
* { id: "edit", label: "Edit", icon: <PencilIcon className="size-4" /> },
|
|
54
|
+
* { id: "duplicate", label: "Duplicate", icon: <CopyIcon className="size-4" /> },
|
|
55
|
+
* { id: "delete", label: "Delete", variant: "destructive", icon: <Trash2Icon className="size-4" /> },
|
|
56
|
+
* ]}
|
|
57
|
+
* >
|
|
58
|
+
* Actions
|
|
59
|
+
* </ActionsDropdown>
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function ActionsDropdown({
|
|
63
|
+
items,
|
|
64
|
+
children = "Actions",
|
|
65
|
+
variant = "outline",
|
|
66
|
+
align = "end",
|
|
67
|
+
className,
|
|
68
|
+
}: ActionsDropdownProps) {
|
|
69
|
+
// Group items by separators (items with id starting with "divider" or "separator")
|
|
70
|
+
const groupedItems: (ActionsDropdownItem | "separator")[] = []
|
|
71
|
+
|
|
72
|
+
items.forEach((item) => {
|
|
73
|
+
if (item.id.startsWith("divider") || item.id.startsWith("separator")) {
|
|
74
|
+
groupedItems.push("separator")
|
|
75
|
+
} else {
|
|
76
|
+
groupedItems.push(item)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<DropdownMenu>
|
|
82
|
+
<DropdownMenuTrigger asChild>
|
|
83
|
+
<Button variant={variant === "primary" ? "default" : variant} className={className}>
|
|
84
|
+
{children}
|
|
85
|
+
<ChevronDownIcon className="ml-2 size-4" />
|
|
86
|
+
</Button>
|
|
87
|
+
</DropdownMenuTrigger>
|
|
88
|
+
<DropdownMenuContent align={align}>
|
|
89
|
+
{groupedItems.map((item, index) => {
|
|
90
|
+
if (item === "separator") {
|
|
91
|
+
return <DropdownMenuSeparator key={`separator-${index}`} />
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<DropdownMenuItem
|
|
96
|
+
key={item.id}
|
|
97
|
+
onClick={item.onClick}
|
|
98
|
+
disabled={item.disabled}
|
|
99
|
+
variant={item.variant}
|
|
100
|
+
>
|
|
101
|
+
{item.icon}
|
|
102
|
+
{item.label}
|
|
103
|
+
</DropdownMenuItem>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</DropdownMenuContent>
|
|
107
|
+
</DropdownMenu>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
Avatar,
|
|
6
|
+
AvatarFallback,
|
|
7
|
+
AvatarImage,
|
|
8
|
+
} from "@/components/ui/avatar"
|
|
9
|
+
import {
|
|
10
|
+
Command,
|
|
11
|
+
CommandDialog,
|
|
12
|
+
CommandEmpty,
|
|
13
|
+
CommandGroup,
|
|
14
|
+
CommandInput,
|
|
15
|
+
CommandItem,
|
|
16
|
+
CommandList,
|
|
17
|
+
} from "@/components/ui/command"
|
|
18
|
+
import {
|
|
19
|
+
Popover,
|
|
20
|
+
PopoverContent,
|
|
21
|
+
PopoverTrigger,
|
|
22
|
+
} from "@/components/ui/popover"
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface Assignee {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
email: string;
|
|
32
|
+
avatar?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AssigneeSelectorProps {
|
|
36
|
+
/** Whether the selector is open */
|
|
37
|
+
open: boolean
|
|
38
|
+
/** Callback when the open state changes */
|
|
39
|
+
onOpenChange: (open: boolean) => void
|
|
40
|
+
/** List of available assignees */
|
|
41
|
+
assignees: Assignee[]
|
|
42
|
+
/** Currently selected assignee ID */
|
|
43
|
+
selectedId?: string
|
|
44
|
+
/** Callback when an assignee is selected */
|
|
45
|
+
onSelect: (assignee: Assignee | null) => void
|
|
46
|
+
/** Placeholder text for search input */
|
|
47
|
+
placeholder?: string
|
|
48
|
+
/** Title for the dialog (for accessibility) */
|
|
49
|
+
title?: string
|
|
50
|
+
/** Allow unassigning (selecting no one) */
|
|
51
|
+
allowUnassign?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Mode: 'popover' renders as anchored popover (for inline use),
|
|
54
|
+
* 'dialog' renders as centered modal (legacy behavior)
|
|
55
|
+
* @default 'popover'
|
|
56
|
+
*/
|
|
57
|
+
mode?: 'popover' | 'dialog'
|
|
58
|
+
/**
|
|
59
|
+
* Trigger element for popover mode. If not provided, the popover will be
|
|
60
|
+
* controlled externally and you must position it yourself.
|
|
61
|
+
*/
|
|
62
|
+
children?: React.ReactNode
|
|
63
|
+
/**
|
|
64
|
+
* Alignment for popover mode
|
|
65
|
+
* @default 'start'
|
|
66
|
+
*/
|
|
67
|
+
align?: 'start' | 'center' | 'end'
|
|
68
|
+
/**
|
|
69
|
+
* Side for popover mode
|
|
70
|
+
* @default 'bottom'
|
|
71
|
+
*/
|
|
72
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Helpers
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
function getInitials(name: string): string {
|
|
80
|
+
const parts = name
|
|
81
|
+
.trim()
|
|
82
|
+
.split(/\s+/)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
|
|
85
|
+
if (parts.length === 0) return "?"
|
|
86
|
+
|
|
87
|
+
return parts
|
|
88
|
+
.slice(0, 2)
|
|
89
|
+
.map((part) => part[0]?.toUpperCase() ?? "")
|
|
90
|
+
.join("")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Component
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
export function AssigneeSelector({
|
|
98
|
+
open,
|
|
99
|
+
onOpenChange,
|
|
100
|
+
assignees,
|
|
101
|
+
selectedId,
|
|
102
|
+
onSelect,
|
|
103
|
+
placeholder = "Search by name or email...",
|
|
104
|
+
title = "Select Assignee",
|
|
105
|
+
allowUnassign = true,
|
|
106
|
+
mode = 'popover',
|
|
107
|
+
children,
|
|
108
|
+
align = 'start',
|
|
109
|
+
side = 'bottom',
|
|
110
|
+
}: AssigneeSelectorProps) {
|
|
111
|
+
const handleSelect = (assignee: Assignee | null) => {
|
|
112
|
+
onSelect(assignee)
|
|
113
|
+
onOpenChange(false)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const commandContent = (
|
|
117
|
+
<Command className="**:data-[selected=true]:bg-muted **:data-selected:bg-transparent">
|
|
118
|
+
<CommandInput placeholder={placeholder} />
|
|
119
|
+
<CommandList>
|
|
120
|
+
<CommandEmpty>No team members found.</CommandEmpty>
|
|
121
|
+
<CommandGroup heading="Team Members">
|
|
122
|
+
{allowUnassign && (
|
|
123
|
+
<CommandItem
|
|
124
|
+
onSelect={() => handleSelect(null)}
|
|
125
|
+
className="gap-2 py-2"
|
|
126
|
+
>
|
|
127
|
+
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
128
|
+
<span className="text-xs">?</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex flex-1 flex-col">
|
|
131
|
+
<span className="text-sm font-medium">Unassigned</span>
|
|
132
|
+
<span className="text-muted-foreground text-xs">
|
|
133
|
+
Remove assignee
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
{!selectedId && (
|
|
137
|
+
<div className="ml-auto text-primary" data-slot="command-shortcut">
|
|
138
|
+
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
139
|
+
<path d="M20 6L9 17l-5-5" />
|
|
140
|
+
</svg>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</CommandItem>
|
|
144
|
+
)}
|
|
145
|
+
{assignees.map((assignee) => (
|
|
146
|
+
<CommandItem
|
|
147
|
+
key={assignee.id}
|
|
148
|
+
value={`${assignee.name} ${assignee.email}`}
|
|
149
|
+
onSelect={() => handleSelect(assignee)}
|
|
150
|
+
className="gap-2 py-2"
|
|
151
|
+
>
|
|
152
|
+
<Avatar className="size-6 shrink-0">
|
|
153
|
+
{assignee.avatar && (
|
|
154
|
+
<AvatarImage src={assignee.avatar} alt={assignee.name} />
|
|
155
|
+
)}
|
|
156
|
+
<AvatarFallback className="text-xs">
|
|
157
|
+
{getInitials(assignee.name)}
|
|
158
|
+
</AvatarFallback>
|
|
159
|
+
</Avatar>
|
|
160
|
+
<div className="flex flex-1 flex-col">
|
|
161
|
+
<span className="text-sm font-medium">{assignee.name}</span>
|
|
162
|
+
<span className="text-muted-foreground text-xs">
|
|
163
|
+
{assignee.email}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
{selectedId === assignee.id && (
|
|
167
|
+
<div className="ml-auto text-primary" data-slot="command-shortcut">
|
|
168
|
+
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
169
|
+
<path d="M20 6L9 17l-5-5" />
|
|
170
|
+
</svg>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</CommandItem>
|
|
174
|
+
))}
|
|
175
|
+
</CommandGroup>
|
|
176
|
+
</CommandList>
|
|
177
|
+
</Command>
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
// Dialog mode (legacy centered modal)
|
|
181
|
+
if (mode === 'dialog') {
|
|
182
|
+
return (
|
|
183
|
+
<CommandDialog
|
|
184
|
+
open={open}
|
|
185
|
+
onOpenChange={onOpenChange}
|
|
186
|
+
title={title}
|
|
187
|
+
description="Search and select a team member to assign"
|
|
188
|
+
showCloseButton={false}
|
|
189
|
+
>
|
|
190
|
+
{commandContent}
|
|
191
|
+
</CommandDialog>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Popover mode (anchored below trigger)
|
|
196
|
+
return (
|
|
197
|
+
<Popover open={open} onOpenChange={onOpenChange}>
|
|
198
|
+
{children && <PopoverTrigger asChild>{children}</PopoverTrigger>}
|
|
199
|
+
<PopoverContent
|
|
200
|
+
className="w-72 p-0"
|
|
201
|
+
align={align}
|
|
202
|
+
side={side}
|
|
203
|
+
sideOffset={4}
|
|
204
|
+
>
|
|
205
|
+
{commandContent}
|
|
206
|
+
</PopoverContent>
|
|
207
|
+
</Popover>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Avatar as AvatarPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Avatar({
|
|
7
|
+
className,
|
|
8
|
+
size = "default",
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|
11
|
+
size?: "default" | "xs" | "sm" | "lg"
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<AvatarPrimitive.Root
|
|
15
|
+
data-slot="avatar"
|
|
16
|
+
data-size={size}
|
|
17
|
+
className={cn(
|
|
18
|
+
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6 data-[size=xs]:size-5",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function AvatarImage({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
30
|
+
return (
|
|
31
|
+
<AvatarPrimitive.Image
|
|
32
|
+
data-slot="avatar-image"
|
|
33
|
+
className={cn("aspect-square size-full", className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function AvatarFallback({
|
|
40
|
+
className,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
43
|
+
return (
|
|
44
|
+
<AvatarPrimitive.Fallback
|
|
45
|
+
data-slot="avatar-fallback"
|
|
46
|
+
className={cn(
|
|
47
|
+
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs group-data-[size=xs]/avatar:text-[10px]",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
|
56
|
+
return (
|
|
57
|
+
<span
|
|
58
|
+
data-slot="avatar-badge"
|
|
59
|
+
className={cn(
|
|
60
|
+
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none",
|
|
61
|
+
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
|
62
|
+
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
|
63
|
+
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="avatar-group"
|
|
75
|
+
className={cn(
|
|
76
|
+
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function AvatarGroupCount({
|
|
85
|
+
className,
|
|
86
|
+
...props
|
|
87
|
+
}: React.ComponentProps<"div">) {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
data-slot="avatar-group-count"
|
|
91
|
+
className={cn(
|
|
92
|
+
"bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
Avatar,
|
|
102
|
+
AvatarImage,
|
|
103
|
+
AvatarFallback,
|
|
104
|
+
AvatarBadge,
|
|
105
|
+
AvatarGroup,
|
|
106
|
+
AvatarGroupCount,
|
|
107
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
8
|
+
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
12
|
+
return (
|
|
13
|
+
<ol
|
|
14
|
+
data-slot="breadcrumb-list"
|
|
15
|
+
className={cn(
|
|
16
|
+
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
25
|
+
return (
|
|
26
|
+
<li
|
|
27
|
+
data-slot="breadcrumb-item"
|
|
28
|
+
className={cn("inline-flex items-center gap-1.5", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function BreadcrumbLink({
|
|
35
|
+
asChild,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<"a"> & {
|
|
39
|
+
asChild?: boolean
|
|
40
|
+
}) {
|
|
41
|
+
const Comp = asChild ? Slot.Root : "a"
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Comp
|
|
45
|
+
data-slot="breadcrumb-link"
|
|
46
|
+
className={cn("hover:text-foreground transition-colors", className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
data-slot="breadcrumb-page"
|
|
56
|
+
role="link"
|
|
57
|
+
aria-disabled="true"
|
|
58
|
+
aria-current="page"
|
|
59
|
+
className={cn("text-foreground font-normal", className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function BreadcrumbSeparator({
|
|
66
|
+
children,
|
|
67
|
+
className,
|
|
68
|
+
...props
|
|
69
|
+
}: React.ComponentProps<"li">) {
|
|
70
|
+
return (
|
|
71
|
+
<li
|
|
72
|
+
data-slot="breadcrumb-separator"
|
|
73
|
+
role="presentation"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
className={cn("[&>svg]:size-3.5 text-muted-foreground", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{children ?? <ChevronRight />}
|
|
79
|
+
</li>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function BreadcrumbEllipsis({
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}: React.ComponentProps<"span">) {
|
|
87
|
+
return (
|
|
88
|
+
<span
|
|
89
|
+
data-slot="breadcrumb-ellipsis"
|
|
90
|
+
role="presentation"
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
className={cn("flex size-9 items-center justify-center", className)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
<MoreHorizontal className="size-4" />
|
|
96
|
+
<span className="sr-only">More</span>
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
Breadcrumb,
|
|
103
|
+
BreadcrumbList,
|
|
104
|
+
BreadcrumbItem,
|
|
105
|
+
BreadcrumbLink,
|
|
106
|
+
BreadcrumbPage,
|
|
107
|
+
BreadcrumbSeparator,
|
|
108
|
+
BreadcrumbEllipsis,
|
|
109
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
2
|
+
import { Slot } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
import { Separator } from "@/components/ui/separator"
|
|
6
|
+
|
|
7
|
+
const buttonGroupVariants = cva(
|
|
8
|
+
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
orientation: {
|
|
12
|
+
horizontal:
|
|
13
|
+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
|
|
14
|
+
vertical:
|
|
15
|
+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
orientation: "horizontal",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function ButtonGroup({
|
|
25
|
+
className,
|
|
26
|
+
orientation,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role="group"
|
|
32
|
+
data-slot="button-group"
|
|
33
|
+
data-orientation={orientation}
|
|
34
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ButtonGroupText({
|
|
41
|
+
className,
|
|
42
|
+
asChild = false,
|
|
43
|
+
...props
|
|
44
|
+
}: React.ComponentProps<"div"> & {
|
|
45
|
+
asChild?: boolean
|
|
46
|
+
}) {
|
|
47
|
+
const Comp = asChild ? Slot.Root : "div"
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
className={cn(
|
|
52
|
+
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ButtonGroupSeparator({
|
|
61
|
+
className,
|
|
62
|
+
orientation = "vertical",
|
|
63
|
+
...props
|
|
64
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
65
|
+
return (
|
|
66
|
+
<Separator
|
|
67
|
+
data-slot="button-group-separator"
|
|
68
|
+
orientation={orientation}
|
|
69
|
+
className={cn(
|
|
70
|
+
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
ButtonGroup,
|
|
80
|
+
ButtonGroupSeparator,
|
|
81
|
+
ButtonGroupText,
|
|
82
|
+
buttonGroupVariants,
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
26
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
27
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
28
|
+
icon: "size-9",
|
|
29
|
+
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
30
|
+
"icon-sm": "size-8",
|
|
31
|
+
"icon-lg": "size-10",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
size: "default",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function Button({
|
|
42
|
+
className,
|
|
43
|
+
variant = "default",
|
|
44
|
+
size = "default",
|
|
45
|
+
asChild = false,
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<"button"> &
|
|
48
|
+
VariantProps<typeof buttonVariants> & {
|
|
49
|
+
asChild?: boolean
|
|
50
|
+
}) {
|
|
51
|
+
const Comp = asChild ? Slot.Root : "button"
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Comp
|
|
55
|
+
data-slot="button"
|
|
56
|
+
data-variant={variant}
|
|
57
|
+
data-size={size}
|
|
58
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Button, buttonVariants }
|