@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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/package.json +86 -0
- package/src/hooks/use-copy-to-clipboard.ts +44 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scrolled.ts +30 -0
- package/src/index.ts +99 -0
- package/src/lib/date-locale.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation-messages.ts +19 -0
- package/src/spree/address-block.tsx +74 -0
- package/src/spree/back-button.tsx +48 -0
- package/src/spree/bulk-dialog.tsx +71 -0
- package/src/spree/bulk-price-table.tsx +254 -0
- package/src/spree/calculator-summary.tsx +120 -0
- package/src/spree/color-picker.tsx +202 -0
- package/src/spree/confirm-dialog.tsx +82 -0
- package/src/spree/copy-to-clipboard-button.tsx +90 -0
- package/src/spree/country-flag.tsx +30 -0
- package/src/spree/data-grid/cells.tsx +415 -0
- package/src/spree/data-grid/context.ts +59 -0
- package/src/spree/data-grid/data-grid.tsx +214 -0
- package/src/spree/data-grid/fill-handle.tsx +153 -0
- package/src/spree/data-grid/helpers.ts +18 -0
- package/src/spree/data-grid/index.ts +5 -0
- package/src/spree/data-grid/types.ts +36 -0
- package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
- package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
- package/src/spree/drag-handle.tsx +51 -0
- package/src/spree/form-actions.tsx +140 -0
- package/src/spree/json-preview-drawer.tsx +254 -0
- package/src/spree/json-value-view.tsx +149 -0
- package/src/spree/language-menu-items.tsx +54 -0
- package/src/spree/metadata/metadata-card.tsx +62 -0
- package/src/spree/relative-time.tsx +49 -0
- package/src/spree/resource-combobox.tsx +108 -0
- package/src/spree/resource-layout.tsx +31 -0
- package/src/spree/resource-multi-autocomplete.tsx +180 -0
- package/src/spree/resource-name-cell.tsx +44 -0
- package/src/spree/route-error-boundary.tsx +102 -0
- package/src/spree/row-actions.tsx +85 -0
- package/src/spree/row-click-bridge.tsx +17 -0
- package/src/spree/secret-input.tsx +137 -0
- package/src/spree/storefront-visible-switch.tsx +58 -0
- package/src/spree/tag-list.tsx +26 -0
- package/src/spree/theme-provider.tsx +83 -0
- package/src/spree/theme-toggle.tsx +35 -0
- package/src/styles.css +338 -0
- package/src/ui/attachment.tsx +187 -0
- package/src/ui/avatar.tsx +94 -0
- package/src/ui/badge.tsx +161 -0
- package/src/ui/breadcrumb.tsx +109 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/calendar.tsx +66 -0
- package/src/ui/card.tsx +72 -0
- package/src/ui/chart.tsx +94 -0
- package/src/ui/checkbox.tsx +50 -0
- package/src/ui/collapsible.tsx +18 -0
- package/src/ui/combobox.tsx +270 -0
- package/src/ui/command.tsx +172 -0
- package/src/ui/data-table.tsx +60 -0
- package/src/ui/date-picker.tsx +253 -0
- package/src/ui/date-range-picker.tsx +150 -0
- package/src/ui/dialog.tsx +214 -0
- package/src/ui/dropdown-menu.tsx +345 -0
- package/src/ui/empty.tsx +94 -0
- package/src/ui/field.tsx +221 -0
- package/src/ui/input-group.tsx +141 -0
- package/src/ui/input.tsx +19 -0
- package/src/ui/label.tsx +29 -0
- package/src/ui/pagination.tsx +123 -0
- package/src/ui/popover.tsx +76 -0
- package/src/ui/progress.tsx +73 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/rich-text-editor.tsx +263 -0
- package/src/ui/select.tsx +213 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/sheet.tsx +208 -0
- package/src/ui/sidebar.tsx +679 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/slot.tsx +35 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/switch.tsx +39 -0
- package/src/ui/textarea.tsx +18 -0
- 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 }
|
package/src/ui/badge.tsx
ADDED
|
@@ -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 }
|