@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.
Files changed (142) hide show
  1. package/.claude/ralph-loop.local.md +9 -0
  2. package/README.md +172 -0
  3. package/bin/init.js +269 -0
  4. package/bun.lock +401 -0
  5. package/components.json +28 -0
  6. package/package.json +74 -0
  7. package/scripts/publish-npm.sh +202 -0
  8. package/src/AppShell.tsx +847 -0
  9. package/src/components/PageHeader.tsx +160 -0
  10. package/src/components/data-table/README.md +447 -0
  11. package/src/components/data-table/data-table-preferences.tsx +184 -0
  12. package/src/components/data-table/data-table-toolbar.tsx +118 -0
  13. package/src/components/data-table/data-table.tsx +37 -0
  14. package/src/components/data-table/index.ts +32 -0
  15. package/src/components/global-header/AllServicesButton.tsx +127 -0
  16. package/src/components/global-header/CategoriesButton.tsx +120 -0
  17. package/src/components/global-header/GlobalHeader.tsx +59 -0
  18. package/src/components/global-header/GlobalHeaderSearch.tsx +57 -0
  19. package/src/components/global-header/HeaderUtilities.tsx +243 -0
  20. package/src/components/global-header/ServicesMenu.tsx +246 -0
  21. package/src/components/layout/AppBreadcrumb.tsx +70 -0
  22. package/src/components/layout/AppFlashbar.tsx +95 -0
  23. package/src/components/layout/AppLayout.tsx +271 -0
  24. package/src/components/layout/AppNavigation.tsx +313 -0
  25. package/src/components/layout/AppSidebar.tsx +229 -0
  26. package/src/components/patterns/index.ts +14 -0
  27. package/src/components/patterns/p-alert-5.tsx +19 -0
  28. package/src/components/patterns/p-autocomplete-5.tsx +89 -0
  29. package/src/components/patterns/p-breadcrumb-1.tsx +28 -0
  30. package/src/components/patterns/p-button-42.tsx +37 -0
  31. package/src/components/patterns/p-button-51.tsx +14 -0
  32. package/src/components/patterns/p-button-6.tsx +5 -0
  33. package/src/components/patterns/p-calendar-1.tsx +18 -0
  34. package/src/components/patterns/p-card-1.tsx +33 -0
  35. package/src/components/patterns/p-card-2.tsx +26 -0
  36. package/src/components/patterns/p-card-5.tsx +31 -0
  37. package/src/components/patterns/p-collapsible-7.tsx +121 -0
  38. package/src/components/patterns/p-command-6.tsx +113 -0
  39. package/src/components/patterns/p-dialog-1.tsx +56 -0
  40. package/src/components/patterns/p-dropdown-menu-1.tsx +38 -0
  41. package/src/components/patterns/p-dropdown-menu-11.tsx +122 -0
  42. package/src/components/patterns/p-dropdown-menu-14.tsx +165 -0
  43. package/src/components/patterns/p-dropdown-menu-9.tsx +108 -0
  44. package/src/components/patterns/p-empty-2.tsx +34 -0
  45. package/src/components/patterns/p-file-upload-1.tsx +72 -0
  46. package/src/components/patterns/p-filters-1.tsx +666 -0
  47. package/src/components/patterns/p-frame-2.tsx +26 -0
  48. package/src/components/patterns/p-tabs-2.tsx +129 -0
  49. package/src/components/reui/alert.tsx +92 -0
  50. package/src/components/reui/autocomplete.tsx +343 -0
  51. package/src/components/reui/badge.tsx +87 -0
  52. package/src/components/reui/data-grid/data-grid-column-filter.tsx +165 -0
  53. package/src/components/reui/data-grid/data-grid-column-header.tsx +339 -0
  54. package/src/components/reui/data-grid/data-grid-column-visibility.tsx +55 -0
  55. package/src/components/reui/data-grid/data-grid-pagination.tsx +224 -0
  56. package/src/components/reui/data-grid/data-grid-table-dnd-rows.tsx +260 -0
  57. package/src/components/reui/data-grid/data-grid-table-dnd.tsx +253 -0
  58. package/src/components/reui/data-grid/data-grid-table.tsx +639 -0
  59. package/src/components/reui/data-grid/data-grid.tsx +209 -0
  60. package/src/components/reui/date-selector.tsx +1330 -0
  61. package/src/components/reui/filters.tsx +1869 -0
  62. package/src/components/reui/frame.tsx +134 -0
  63. package/src/components/reui/index.ts +17 -0
  64. package/src/components/reui/timeline.tsx +219 -0
  65. package/src/components/search/Autocomplete.tsx +183 -0
  66. package/src/components/search/AutocompleteClient.tsx +293 -0
  67. package/src/components/search/GlobalSearch.tsx +187 -0
  68. package/src/components/section-drawer/deal-drawer-content.tsx +891 -0
  69. package/src/components/section-drawer/index.ts +19 -0
  70. package/src/components/section-drawer/section-drawer.css +665 -0
  71. package/src/components/section-drawer/section-drawer.tsx +467 -0
  72. package/src/components/sectioned-list-board/README.md +78 -0
  73. package/src/components/sectioned-list-board/board-card-content.tsx +340 -0
  74. package/src/components/sectioned-list-board/date-range-filter.tsx +249 -0
  75. package/src/components/sectioned-list-board/index.ts +19 -0
  76. package/src/components/sectioned-list-board/sectioned-list-board.css +564 -0
  77. package/src/components/sectioned-list-board/sectioned-list-board.tsx +731 -0
  78. package/src/components/sectioned-list-board/sortable-card.tsx +314 -0
  79. package/src/components/sectioned-list-board/sortable-section.tsx +319 -0
  80. package/src/components/sectioned-list-board/types.ts +216 -0
  81. package/src/components/sectioned-list-table/README.md +80 -0
  82. package/src/components/sectioned-list-table/index.ts +14 -0
  83. package/src/components/sectioned-list-table/sectioned-list-table.css +534 -0
  84. package/src/components/sectioned-list-table/sectioned-list-table.tsx +740 -0
  85. package/src/components/sectioned-list-table/sortable-column-header.tsx +120 -0
  86. package/src/components/sectioned-list-table/sortable-row.tsx +420 -0
  87. package/src/components/sectioned-list-table/sortable-section.tsx +251 -0
  88. package/src/components/sectioned-list-table/table-cell-content.tsx +129 -0
  89. package/src/components/sectioned-list-table/types.ts +120 -0
  90. package/src/components/sectioned-list-table/use-column-preferences.ts +103 -0
  91. package/src/components/ui/actions-dropdown.tsx +109 -0
  92. package/src/components/ui/assignee-selector.tsx +209 -0
  93. package/src/components/ui/avatar.tsx +107 -0
  94. package/src/components/ui/breadcrumb.tsx +109 -0
  95. package/src/components/ui/button-group.tsx +83 -0
  96. package/src/components/ui/button.tsx +64 -0
  97. package/src/components/ui/calendar.tsx +220 -0
  98. package/src/components/ui/card.tsx +92 -0
  99. package/src/components/ui/chart.tsx +376 -0
  100. package/src/components/ui/checkbox.tsx +30 -0
  101. package/src/components/ui/collapsible.tsx +33 -0
  102. package/src/components/ui/command.tsx +182 -0
  103. package/src/components/ui/context-menu.tsx +250 -0
  104. package/src/components/ui/create-button-group.tsx +128 -0
  105. package/src/components/ui/dialog.tsx +156 -0
  106. package/src/components/ui/drawer.tsx +133 -0
  107. package/src/components/ui/dropdown-menu.tsx +255 -0
  108. package/src/components/ui/empty.tsx +104 -0
  109. package/src/components/ui/field.tsx +248 -0
  110. package/src/components/ui/form.tsx +165 -0
  111. package/src/components/ui/index.ts +37 -0
  112. package/src/components/ui/input-group.tsx +168 -0
  113. package/src/components/ui/input.tsx +21 -0
  114. package/src/components/ui/kbd.tsx +28 -0
  115. package/src/components/ui/label.tsx +22 -0
  116. package/src/components/ui/navigation-menu.tsx +168 -0
  117. package/src/components/ui/page-header.tsx +80 -0
  118. package/src/components/ui/popover.tsx +87 -0
  119. package/src/components/ui/scroll-area.tsx +56 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +26 -0
  122. package/src/components/ui/sheet.tsx +141 -0
  123. package/src/components/ui/sidebar.tsx +726 -0
  124. package/src/components/ui/skeleton.tsx +13 -0
  125. package/src/components/ui/sonner.tsx +38 -0
  126. package/src/components/ui/switch.tsx +33 -0
  127. package/src/components/ui/tabs.tsx +91 -0
  128. package/src/components/ui/textarea.tsx +18 -0
  129. package/src/components/ui/toggle-group.tsx +83 -0
  130. package/src/components/ui/toggle.tsx +45 -0
  131. package/src/components/ui/tooltip.tsx +57 -0
  132. package/src/hooks/use-copy-to-clipboard.ts +37 -0
  133. package/src/hooks/use-file-upload.ts +415 -0
  134. package/src/hooks/use-mobile.ts +19 -0
  135. package/src/index.ts +95 -0
  136. package/src/lib/utils.ts +6 -0
  137. package/src/styles.css +1859 -0
  138. package/src/urls.ts +83 -0
  139. package/src/vite.d.ts +22 -0
  140. package/src/vite.js +241 -0
  141. package/tsconfig.base.json +18 -0
  142. 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 }