@spree/dashboard-ui 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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/package.json +86 -0
  4. package/src/hooks/use-copy-to-clipboard.ts +44 -0
  5. package/src/hooks/use-mobile.ts +19 -0
  6. package/src/hooks/use-scrolled.ts +30 -0
  7. package/src/index.ts +99 -0
  8. package/src/lib/date-locale.ts +29 -0
  9. package/src/lib/utils.ts +6 -0
  10. package/src/lib/validation-messages.ts +19 -0
  11. package/src/spree/address-block.tsx +74 -0
  12. package/src/spree/back-button.tsx +48 -0
  13. package/src/spree/bulk-dialog.tsx +71 -0
  14. package/src/spree/bulk-price-table.tsx +254 -0
  15. package/src/spree/calculator-summary.tsx +120 -0
  16. package/src/spree/color-picker.tsx +202 -0
  17. package/src/spree/confirm-dialog.tsx +82 -0
  18. package/src/spree/copy-to-clipboard-button.tsx +90 -0
  19. package/src/spree/country-flag.tsx +30 -0
  20. package/src/spree/data-grid/cells.tsx +415 -0
  21. package/src/spree/data-grid/context.ts +59 -0
  22. package/src/spree/data-grid/data-grid.tsx +214 -0
  23. package/src/spree/data-grid/fill-handle.tsx +153 -0
  24. package/src/spree/data-grid/helpers.ts +18 -0
  25. package/src/spree/data-grid/index.ts +5 -0
  26. package/src/spree/data-grid/types.ts +36 -0
  27. package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
  28. package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
  29. package/src/spree/drag-handle.tsx +51 -0
  30. package/src/spree/form-actions.tsx +140 -0
  31. package/src/spree/json-preview-drawer.tsx +254 -0
  32. package/src/spree/json-value-view.tsx +149 -0
  33. package/src/spree/language-menu-items.tsx +54 -0
  34. package/src/spree/metadata/metadata-card.tsx +62 -0
  35. package/src/spree/relative-time.tsx +49 -0
  36. package/src/spree/resource-combobox.tsx +108 -0
  37. package/src/spree/resource-layout.tsx +31 -0
  38. package/src/spree/resource-multi-autocomplete.tsx +180 -0
  39. package/src/spree/resource-name-cell.tsx +44 -0
  40. package/src/spree/route-error-boundary.tsx +102 -0
  41. package/src/spree/row-actions.tsx +85 -0
  42. package/src/spree/row-click-bridge.tsx +17 -0
  43. package/src/spree/secret-input.tsx +137 -0
  44. package/src/spree/storefront-visible-switch.tsx +58 -0
  45. package/src/spree/tag-list.tsx +26 -0
  46. package/src/spree/theme-provider.tsx +83 -0
  47. package/src/spree/theme-toggle.tsx +35 -0
  48. package/src/styles.css +338 -0
  49. package/src/ui/attachment.tsx +187 -0
  50. package/src/ui/avatar.tsx +94 -0
  51. package/src/ui/badge.tsx +161 -0
  52. package/src/ui/breadcrumb.tsx +109 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/calendar.tsx +66 -0
  55. package/src/ui/card.tsx +72 -0
  56. package/src/ui/chart.tsx +94 -0
  57. package/src/ui/checkbox.tsx +50 -0
  58. package/src/ui/collapsible.tsx +18 -0
  59. package/src/ui/combobox.tsx +270 -0
  60. package/src/ui/command.tsx +172 -0
  61. package/src/ui/data-table.tsx +60 -0
  62. package/src/ui/date-picker.tsx +253 -0
  63. package/src/ui/date-range-picker.tsx +150 -0
  64. package/src/ui/dialog.tsx +214 -0
  65. package/src/ui/dropdown-menu.tsx +345 -0
  66. package/src/ui/empty.tsx +94 -0
  67. package/src/ui/field.tsx +221 -0
  68. package/src/ui/input-group.tsx +141 -0
  69. package/src/ui/input.tsx +19 -0
  70. package/src/ui/label.tsx +29 -0
  71. package/src/ui/pagination.tsx +123 -0
  72. package/src/ui/popover.tsx +76 -0
  73. package/src/ui/progress.tsx +73 -0
  74. package/src/ui/radio-group.tsx +42 -0
  75. package/src/ui/rich-text-editor.tsx +263 -0
  76. package/src/ui/select.tsx +213 -0
  77. package/src/ui/separator.tsx +31 -0
  78. package/src/ui/sheet.tsx +208 -0
  79. package/src/ui/sidebar.tsx +679 -0
  80. package/src/ui/skeleton.tsx +13 -0
  81. package/src/ui/slot.tsx +35 -0
  82. package/src/ui/sonner.tsx +47 -0
  83. package/src/ui/switch.tsx +39 -0
  84. package/src/ui/textarea.tsx +18 -0
  85. package/src/ui/tooltip.tsx +100 -0
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ import { mergeProps } from '@base-ui/react/merge-props'
4
+ import { useRender } from '@base-ui/react/use-render'
5
+ import { cva, type VariantProps } from 'class-variance-authority'
6
+ import type * as React from 'react'
7
+
8
+ import { cn } from '../lib/utils'
9
+ import { Button } from './button'
10
+
11
+ // Ported from the shadcn Base UI `attachment` registry component, with the
12
+ // `cn-*` preset classes flattened into plain utilities (this package predates
13
+ // the shadcn CSS layer).
14
+ const attachmentVariants = cva(
15
+ 'group/attachment relative flex max-w-full min-w-0 shrink-0 flex-wrap rounded-lg border bg-card text-card-foreground transition-colors has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
16
+ {
17
+ variants: {
18
+ size: {
19
+ default: 'gap-2.5 p-2 pr-2.5',
20
+ sm: 'gap-2 p-1.5 pr-2',
21
+ xs: 'gap-1.5 p-1 pr-1.5',
22
+ },
23
+ orientation: {
24
+ horizontal: 'items-center',
25
+ vertical: 'flex-col',
26
+ },
27
+ },
28
+ },
29
+ )
30
+
31
+ function Attachment({
32
+ className,
33
+ state = 'done',
34
+ size = 'default',
35
+ orientation = 'horizontal',
36
+ ...props
37
+ }: React.ComponentProps<'div'> &
38
+ VariantProps<typeof attachmentVariants> & {
39
+ state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done'
40
+ }) {
41
+ return (
42
+ <div
43
+ data-slot="attachment"
44
+ data-state={state}
45
+ data-size={size}
46
+ data-orientation={orientation}
47
+ className={cn(attachmentVariants({ size, orientation }), className)}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ const attachmentMediaVariants = cva(
54
+ 'relative flex aspect-square size-9 shrink-0 items-center justify-center overflow-hidden rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4',
55
+ {
56
+ variants: {
57
+ variant: {
58
+ icon: 'bg-muted text-muted-foreground',
59
+ // Image fit is left to the caller's <img> classes — logos want
60
+ // `object-contain`, photos want `object-cover`.
61
+ image: 'bg-muted',
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: 'icon',
66
+ },
67
+ },
68
+ )
69
+
70
+ function AttachmentMedia({
71
+ className,
72
+ variant = 'icon',
73
+ ...props
74
+ }: React.ComponentProps<'div'> & VariantProps<typeof attachmentMediaVariants>) {
75
+ return (
76
+ <div
77
+ data-slot="attachment-media"
78
+ data-variant={variant}
79
+ className={cn(attachmentMediaVariants({ variant }), className)}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function AttachmentContent({ className, ...props }: React.ComponentProps<'div'>) {
86
+ return (
87
+ <div
88
+ data-slot="attachment-content"
89
+ className={cn('max-w-full min-w-0 flex-1', className)}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>) {
96
+ return (
97
+ <span
98
+ data-slot="attachment-title"
99
+ className={cn('block max-w-full min-w-0 truncate font-medium text-sm', className)}
100
+ {...props}
101
+ />
102
+ )
103
+ }
104
+
105
+ function AttachmentDescription({ className, ...props }: React.ComponentProps<'span'>) {
106
+ return (
107
+ <span
108
+ data-slot="attachment-description"
109
+ className={cn(
110
+ 'block max-w-full min-w-0 truncate text-muted-foreground text-xs group-data-[state=error]/attachment:text-destructive/80',
111
+ className,
112
+ )}
113
+ {...props}
114
+ />
115
+ )
116
+ }
117
+
118
+ function AttachmentActions({ className, ...props }: React.ComponentProps<'div'>) {
119
+ return (
120
+ <div
121
+ data-slot="attachment-actions"
122
+ className={cn('flex shrink-0 items-center gap-1', className)}
123
+ {...props}
124
+ />
125
+ )
126
+ }
127
+
128
+ function AttachmentAction({
129
+ className,
130
+ variant,
131
+ size = 'icon-xs',
132
+ ...props
133
+ }: React.ComponentProps<typeof Button>) {
134
+ return (
135
+ <Button
136
+ data-slot="attachment-action"
137
+ variant={variant ?? 'ghost'}
138
+ size={size}
139
+ className={className}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ function AttachmentTrigger({
146
+ className,
147
+ render,
148
+ type,
149
+ ...props
150
+ }: useRender.ComponentProps<'button'>) {
151
+ return useRender({
152
+ defaultTagName: 'button',
153
+ props: mergeProps<'button'>(
154
+ {
155
+ type: render ? type : (type ?? 'button'),
156
+ className: cn('absolute inset-0 z-10 outline-none', className),
157
+ },
158
+ props,
159
+ ),
160
+ render,
161
+ state: {
162
+ slot: 'attachment-trigger',
163
+ },
164
+ })
165
+ }
166
+
167
+ function AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>) {
168
+ return (
169
+ <div
170
+ data-slot="attachment-group"
171
+ className={cn('flex min-w-0 gap-2 overflow-x-auto', className)}
172
+ {...props}
173
+ />
174
+ )
175
+ }
176
+
177
+ export {
178
+ Attachment,
179
+ AttachmentAction,
180
+ AttachmentActions,
181
+ AttachmentContent,
182
+ AttachmentDescription,
183
+ AttachmentGroup,
184
+ AttachmentMedia,
185
+ AttachmentTitle,
186
+ AttachmentTrigger,
187
+ }
@@ -0,0 +1,94 @@
1
+ import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar'
2
+ import type * as React from 'react'
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' | '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 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
19
+ className,
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
27
+ return (
28
+ <AvatarPrimitive.Image
29
+ data-slot="avatar-image"
30
+ className={cn('aspect-square size-full rounded-full object-cover', className)}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function AvatarFallback({
37
+ className,
38
+ ...props
39
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
40
+ return (
41
+ <AvatarPrimitive.Fallback
42
+ data-slot="avatar-fallback"
43
+ className={cn(
44
+ 'flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs uppercase',
45
+ className,
46
+ )}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
53
+ return (
54
+ <span
55
+ data-slot="avatar-badge"
56
+ className={cn(
57
+ 'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-lg bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
58
+ 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
59
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
60
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
69
+ return (
70
+ <div
71
+ data-slot="avatar-group"
72
+ className={cn(
73
+ 'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
82
+ return (
83
+ <div
84
+ data-slot="avatar-group-count"
85
+ className={cn(
86
+ 'relative flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-sm text-muted-foreground ring-2 ring-background 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',
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage }
@@ -0,0 +1,161 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { CheckIcon } from 'lucide-react'
3
+ import * as React from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { cn } from '../lib/utils'
6
+ import { Slot } from './slot'
7
+
8
+ const badgeVariants = cva(
9
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
14
+ secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
15
+ destructive:
16
+ 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
17
+ // Mirrors the `destructive` recipe (tinted bg + matching text) with a
18
+ // green hue for "succeeded / completed / paid / shipped" states. Dark
19
+ // mode bumps text to `green-400` so it stays legible on dark fills.
20
+ success:
21
+ 'bg-green-500/15 text-green-700 dark:bg-green-500/15 dark:text-green-400 [a]:hover:bg-green-500/25',
22
+ outline:
23
+ 'border-border text-foreground/75 [a]:hover:bg-muted [a]:hover:text-muted-foreground',
24
+ ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
25
+ link: 'text-primary underline-offset-4 hover:underline',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: 'default',
30
+ },
31
+ },
32
+ )
33
+
34
+ function Badge({
35
+ className,
36
+ variant = 'default',
37
+ asChild = false,
38
+ ...props
39
+ }: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
40
+ const Comp = asChild ? Slot : 'span'
41
+
42
+ return (
43
+ <Comp
44
+ data-slot="badge"
45
+ data-variant={variant}
46
+ className={cn(badgeVariants({ variant }), className)}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ // Maps order/payment/etc. status strings to one of the canonical Badge
53
+ // variants. Success-y states ("active", "paid", "shipped", …) route through
54
+ // the `success` variant so the UI stays color-coded; everything else leans
55
+ // on the canonical shadcn variants with shape + label carrying meaning.
56
+ const statusVariantMap: Record<string, VariantProps<typeof badgeVariants>['variant']> = {
57
+ active: 'success',
58
+ complete: 'success',
59
+ completed: 'success',
60
+ paid: 'success',
61
+ shipped: 'success',
62
+ published: 'success',
63
+ approved: 'success',
64
+ ready: 'success',
65
+ draft: 'outline',
66
+ pending: 'outline',
67
+ processing: 'outline',
68
+ inactive: 'outline',
69
+ cart: 'outline',
70
+ address: 'outline',
71
+ delivery: 'outline',
72
+ payment: 'outline',
73
+ confirm: 'outline',
74
+ resumed: 'secondary',
75
+ partial: 'secondary',
76
+ archived: 'secondary',
77
+ returned: 'secondary',
78
+ backorder: 'secondary',
79
+ balance_due: 'secondary',
80
+ credit_owed: 'secondary',
81
+ canceled: 'destructive',
82
+ failed: 'destructive',
83
+ void: 'destructive',
84
+ error: 'destructive',
85
+ rejected: 'destructive',
86
+ }
87
+
88
+ /**
89
+ * Status pill whose color is derived from the raw status code. Stays headless:
90
+ * pass a translated `label` from the app layer; without one it humanizes the
91
+ * code itself (`balance_due` → `balance due`) as a best-effort fallback.
92
+ */
93
+ function StatusBadge({
94
+ status,
95
+ label,
96
+ className,
97
+ }: {
98
+ status: string
99
+ label?: string
100
+ className?: string
101
+ }) {
102
+ const variant = statusVariantMap[status] ?? 'outline'
103
+ return (
104
+ <Badge variant={variant} className={cn('capitalize', className)}>
105
+ {label ?? status.replace(/_/g, ' ')}
106
+ </Badge>
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Boolean-state badge — mirrors the Rails admin's `active_badge` helper.
112
+ *
113
+ * Renders a `success` (check + label) badge when `active`, an `outline`
114
+ * badge with the inactive label otherwise. Use it for "Yes/No" or
115
+ * "Enabled/Disabled" cells in tables and detail rows.
116
+ *
117
+ * @example Default Yes/No
118
+ * <ActiveBadge active={user.confirmed} />
119
+ *
120
+ * @example Custom labels
121
+ * <ActiveBadge active={sl.pickup_enabled} activeLabel="Enabled" inactiveLabel="Disabled" />
122
+ *
123
+ * @example Hide inactive (renders a muted dash instead — matches the
124
+ * pre-existing `<Badge> : <span>—</span>` pattern in stock-locations).
125
+ * <ActiveBadge active={sl.pickup_enabled} activeLabel="Enabled" dashWhenInactive />
126
+ */
127
+ function ActiveBadge({
128
+ active,
129
+ activeLabel,
130
+ inactiveLabel,
131
+ dashWhenInactive = false,
132
+ className,
133
+ }: {
134
+ active: boolean | null | undefined
135
+ activeLabel?: string
136
+ inactiveLabel?: string
137
+ dashWhenInactive?: boolean
138
+ className?: string
139
+ }) {
140
+ const { t } = useTranslation()
141
+ const resolvedActiveLabel = activeLabel ?? t('admin.common.yes')
142
+ const resolvedInactiveLabel = inactiveLabel ?? t('admin.common.no')
143
+ if (active) {
144
+ return (
145
+ <Badge variant="success" className={className}>
146
+ <CheckIcon />
147
+ {resolvedActiveLabel}
148
+ </Badge>
149
+ )
150
+ }
151
+ if (dashWhenInactive) {
152
+ return <span className={cn('text-muted-foreground', className)}>—</span>
153
+ }
154
+ return (
155
+ <Badge variant="outline" className={className}>
156
+ {resolvedInactiveLabel}
157
+ </Badge>
158
+ )
159
+ }
160
+
161
+ export { ActiveBadge, Badge, badgeVariants, StatusBadge }
@@ -0,0 +1,109 @@
1
+ import i18n from 'i18next'
2
+ import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
3
+ import type * as React from 'react'
4
+ import { cn } from '../lib/utils'
5
+ import { Slot } from './slot'
6
+
7
+ function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
8
+ return (
9
+ <nav
10
+ aria-label={i18n.t('admin.a11y.breadcrumb')}
11
+ data-slot="breadcrumb"
12
+ className={cn(className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
19
+ return (
20
+ <ol
21
+ data-slot="breadcrumb-list"
22
+ className={cn(
23
+ 'flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground',
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
32
+ return (
33
+ <li
34
+ data-slot="breadcrumb-item"
35
+ className={cn('inline-flex items-center gap-1', className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function BreadcrumbLink({
42
+ asChild,
43
+ className,
44
+ ...props
45
+ }: React.ComponentProps<'a'> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? Slot : 'a'
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="breadcrumb-link"
53
+ className={cn('transition-colors hover:text-foreground', className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
60
+ return (
61
+ <span
62
+ data-slot="breadcrumb-page"
63
+ role="link"
64
+ aria-disabled="true"
65
+ aria-current="page"
66
+ className={cn('font-normal text-foreground', className)}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
73
+ return (
74
+ <li
75
+ data-slot="breadcrumb-separator"
76
+ role="presentation"
77
+ aria-hidden="true"
78
+ className={cn('[&>svg]:size-3.5', className)}
79
+ {...props}
80
+ >
81
+ {children ?? <ChevronRightIcon />}
82
+ </li>
83
+ )
84
+ }
85
+
86
+ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn('flex size-5 items-center justify-center [&>svg]:size-4', className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontalIcon />
96
+ <span className="sr-only">{i18n.t('admin.a11y.breadcrumb_more')}</span>
97
+ </span>
98
+ )
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbEllipsis,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbList,
107
+ BreadcrumbPage,
108
+ BreadcrumbSeparator,
109
+ }
@@ -0,0 +1,60 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import type * as React from 'react'
3
+ import { cn } from '../lib/utils'
4
+ import { Slot } from './slot'
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-xl border border-transparent text-sm font-medium whitespace-nowrap cursor-pointer select-none no-underline transition-[color,background-color,border-color,box-shadow] duration-100 ease-linear outline-none focus-visible:border-blue-500 focus-visible:shadow-[0_0_0_3px_rgba(59,130,246,0.15)] disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/85 dark:bg-blue-600 dark:text-white dark:hover:bg-blue-500',
13
+ outline:
14
+ 'border-border bg-muted text-foreground shadow-xs hover:bg-accent hover:text-foreground aria-expanded:bg-accent',
15
+ ghost: 'hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent',
16
+ destructive: 'text-destructive bg-muted border-border shadow-xs hover:bg-destructive/10',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-9 px-3 py-1.5',
21
+ xs: "h-6 gap-1 rounded-lg px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
22
+ sm: "h-7 gap-1 rounded-lg px-2 py-1 text-sm [&_svg:not([class*='size-'])]:size-3.5",
23
+ lg: 'h-10 px-3 py-2 text-base',
24
+ icon: 'size-9',
25
+ 'icon-xs': "size-6 rounded-lg [&_svg:not([class*='size-'])]:size-3",
26
+ 'icon-sm': 'size-7 rounded-lg',
27
+ 'icon-lg': 'size-10',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: 'default',
32
+ size: 'default',
33
+ },
34
+ },
35
+ )
36
+
37
+ function Button({
38
+ className,
39
+ variant = 'default',
40
+ size = 'default',
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<'button'> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean
46
+ }) {
47
+ const Comp = asChild ? Slot : 'button'
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ data-variant={variant}
53
+ data-size={size}
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export { Button, buttonVariants }
@@ -0,0 +1,66 @@
1
+ import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
2
+ import type * as React from 'react'
3
+ import { DayPicker } from 'react-day-picker'
4
+ import { activeDateLocale } from '../lib/date-locale'
5
+ import { cn } from '../lib/utils'
6
+
7
+ function Calendar({
8
+ className,
9
+ classNames,
10
+ showOutsideDays = true,
11
+ locale,
12
+ ...props
13
+ }: React.ComponentProps<typeof DayPicker>) {
14
+ return (
15
+ <DayPicker
16
+ // Month/weekday names follow the admin UI language unless a caller
17
+ // overrides `locale`.
18
+ locale={locale ?? activeDateLocale()}
19
+ showOutsideDays={showOutsideDays}
20
+ className={cn('p-3', className)}
21
+ classNames={{
22
+ months: 'flex flex-col sm:flex-row gap-2 relative',
23
+ month: 'flex flex-col gap-4',
24
+ month_caption: 'flex justify-center pt-1 relative items-center w-full',
25
+ caption_label: 'text-sm font-medium',
26
+ nav: 'absolute top-1 inset-x-0 flex items-center justify-between px-1 z-10',
27
+ button_previous:
28
+ 'inline-flex size-7 items-center justify-center rounded-md border border-border bg-transparent p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
29
+ button_next:
30
+ 'inline-flex size-7 items-center justify-center rounded-md border border-border bg-transparent p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
31
+ month_grid: 'w-full border-collapse',
32
+ weekdays: 'flex',
33
+ weekday: 'text-muted-foreground rounded-md w-8 font-normal text-xs',
34
+ week: 'flex w-full mt-2',
35
+ day: 'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-blue-50 [&:has([aria-selected].day-outside)]:bg-blue-50/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
36
+ day_button: cn(
37
+ 'inline-flex size-8 items-center justify-center rounded-md p-0 font-normal transition-colors',
38
+ 'hover:bg-accent hover:text-foreground',
39
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
40
+ 'aria-selected:bg-blue-600 aria-selected:text-white aria-selected:hover:bg-blue-700 aria-selected:focus:bg-blue-700',
41
+ ),
42
+ range_start: 'day-range-start rounded-l-md',
43
+ range_end: 'day-range-end rounded-r-md',
44
+ selected: 'bg-blue-600 text-white hover:bg-blue-700 focus:bg-blue-700',
45
+ today: 'bg-accent text-foreground',
46
+ outside:
47
+ 'day-outside text-muted-foreground opacity-50 aria-selected:bg-blue-500/15 aria-selected:text-muted-foreground aria-selected:opacity-60',
48
+ disabled: 'text-muted-foreground opacity-50',
49
+ range_middle: 'aria-selected:bg-blue-500/15 aria-selected:text-foreground',
50
+ hidden: 'invisible',
51
+ ...classNames,
52
+ }}
53
+ components={{
54
+ Chevron: ({ orientation }) =>
55
+ orientation === 'left' ? (
56
+ <ChevronLeftIcon className="size-4" />
57
+ ) : (
58
+ <ChevronRightIcon className="size-4" />
59
+ ),
60
+ }}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Calendar }