@spree/dashboard-ui 0.13.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +15 -9
- package/src/hooks/use-prefers-reduced-motion.ts +26 -0
- package/src/hooks/use-scrolled.ts +34 -14
- package/src/index.ts +39 -1
- package/src/lib/date-range-presets.ts +79 -0
- package/src/lib/same-rich-text.ts +145 -0
- package/src/spree/address-block.tsx +5 -4
- package/src/spree/address-book-row.tsx +96 -0
- package/src/spree/address-map.tsx +62 -0
- package/src/spree/back-button.tsx +14 -9
- package/src/spree/bulk-dialog.tsx +2 -2
- package/src/spree/bulk-price-table.tsx +145 -14
- package/src/spree/bulk-variants-table.tsx +434 -0
- package/src/spree/calculator-summary.tsx +5 -1
- package/src/spree/claim-resolve-dialog.tsx +219 -0
- package/src/spree/confirm-dialog.tsx +1 -1
- package/src/spree/copy-to-clipboard-button.tsx +1 -1
- package/src/spree/data-grid/cells.tsx +199 -25
- package/src/spree/data-grid/context.ts +6 -0
- package/src/spree/data-grid/data-grid.tsx +149 -7
- package/src/spree/data-grid/helpers.ts +6 -2
- package/src/spree/data-grid/index.ts +9 -1
- package/src/spree/data-grid/use-data-grid-keyboard.ts +4 -2
- package/src/spree/delivery-form-dialog.tsx +166 -0
- package/src/spree/drag-handle.tsx +2 -2
- package/src/spree/editable-note-card.tsx +103 -0
- package/src/spree/external-link.tsx +56 -0
- package/src/spree/file-type-icon.tsx +68 -0
- package/src/spree/form-actions.tsx +2 -3
- package/src/spree/form-section.tsx +46 -0
- package/src/spree/fulfill-items-form.tsx +282 -0
- package/src/spree/fulfillment-edit-dialog.tsx +211 -0
- package/src/spree/fulfillment-item-list.tsx +61 -0
- package/src/spree/fulfillment-panel.tsx +53 -0
- package/src/spree/icons.ts +167 -0
- package/src/spree/json-preview-drawer.tsx +4 -4
- package/src/spree/label-upload-dialog.tsx +195 -0
- package/src/spree/language-menu-items.tsx +37 -30
- package/src/spree/metadata/metadata-card.tsx +1 -1
- package/src/spree/order-cancel-dialog.tsx +149 -0
- package/src/spree/post-sale-create-dialogs.tsx +253 -0
- package/src/spree/post-sale-fields.tsx +133 -0
- package/src/spree/product-membership-list.tsx +453 -0
- package/src/spree/products/status-card.tsx +79 -0
- package/src/spree/quantity-tier-editor.tsx +162 -0
- package/src/spree/resource-combobox.tsx +12 -1
- package/src/spree/resource-name-cell.tsx +21 -4
- package/src/spree/return-dialogs.tsx +238 -0
- package/src/spree/route-error-boundary.tsx +1 -1
- package/src/spree/row-actions.tsx +2 -2
- package/src/spree/search-input.tsx +76 -0
- package/src/spree/secret-input.tsx +26 -40
- package/src/spree/tax-identifiers-card.tsx +418 -0
- package/src/spree/theme-toggle.tsx +36 -23
- package/src/spree/wizard.tsx +254 -0
- package/src/styles.css +750 -75
- package/src/ui/accordion.tsx +83 -0
- package/src/ui/alert.tsx +79 -0
- package/src/ui/attachment.tsx +1 -1
- package/src/ui/badge.tsx +198 -56
- package/src/ui/breadcrumb.tsx +1 -1
- package/src/ui/button.tsx +22 -10
- package/src/ui/calendar.tsx +1 -1
- package/src/ui/card.tsx +52 -6
- package/src/ui/checkbox.tsx +13 -5
- package/src/ui/combobox.tsx +125 -6
- package/src/ui/command.tsx +149 -23
- package/src/ui/context-menu.tsx +99 -0
- package/src/ui/data-table.tsx +448 -9
- package/src/ui/date-picker.tsx +18 -3
- package/src/ui/date-range-picker.tsx +47 -60
- package/src/ui/dialog.tsx +6 -6
- package/src/ui/dropdown-menu.tsx +18 -11
- package/src/ui/field.tsx +10 -2
- package/src/ui/input-group.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/map.tsx +2110 -0
- package/src/ui/media-preview.tsx +105 -0
- package/src/ui/pagination.tsx +19 -11
- package/src/ui/popover.tsx +4 -2
- package/src/ui/progress.tsx +5 -2
- package/src/ui/radio-group.tsx +4 -4
- package/src/ui/rich-text-editor.tsx +214 -88
- package/src/ui/scroll-area.tsx +65 -0
- package/src/ui/select.tsx +17 -10
- package/src/ui/separator.tsx +1 -1
- package/src/ui/sheet.tsx +49 -7
- package/src/ui/sidebar.tsx +138 -37
- package/src/ui/skeleton.tsx +3 -1
- package/src/ui/tabs.tsx +84 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/thumbnail.tsx +80 -0
- package/src/ui/toast.tsx +112 -0
- package/src/ui/tooltip.tsx +3 -1
- package/src/ui/sonner.tsx +0 -47
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
import { ChevronDownIcon } from '../spree/icons'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vertically stacked, expandable sections sharing one frame.
|
|
8
|
+
*
|
|
9
|
+
* One item is open at a time by default: a list of steps is read in order, and
|
|
10
|
+
* letting several stand open turns it back into the wall of text the collapse
|
|
11
|
+
* exists to avoid. Pass `multiple` where the sections are independent rather
|
|
12
|
+
* than sequential.
|
|
13
|
+
*/
|
|
14
|
+
function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
15
|
+
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One section. `value` identifies it for `defaultValue` and controlled use. */
|
|
19
|
+
function AccordionItem({
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
23
|
+
return (
|
|
24
|
+
<AccordionPrimitive.Item
|
|
25
|
+
data-slot="accordion-item"
|
|
26
|
+
// Rules between items rather than around each: the group is one object,
|
|
27
|
+
// so the last item closes on the container's own edge.
|
|
28
|
+
className={cn('border-border-subtle border-b last:border-b-0', className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The always-visible row that opens its section. Renders a chevron after the
|
|
36
|
+
* children, so callers pass only the label's own content.
|
|
37
|
+
*/
|
|
38
|
+
function AccordionTrigger({
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
43
|
+
return (
|
|
44
|
+
<AccordionPrimitive.Header className="flex">
|
|
45
|
+
<AccordionPrimitive.Trigger
|
|
46
|
+
data-slot="accordion-trigger"
|
|
47
|
+
className={cn(
|
|
48
|
+
'group/accordion-trigger flex flex-1 cursor-pointer items-center gap-3 px-4 py-3.5 text-left text-base font-medium transition-colors outline-none hover:bg-accent/50 focus-visible:bg-accent',
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
<ChevronDownIcon className="ms-auto size-4 shrink-0 text-muted-foreground transition-transform duration-200 ease-out group-data-[panel-open]/accordion-trigger:rotate-180 motion-reduce:transition-none" />
|
|
55
|
+
</AccordionPrimitive.Trigger>
|
|
56
|
+
</AccordionPrimitive.Header>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The revealed panel. Carries the section's padding, so `className` styles the
|
|
62
|
+
* content rather than the animated box around it.
|
|
63
|
+
*/
|
|
64
|
+
function AccordionContent({
|
|
65
|
+
className,
|
|
66
|
+
children,
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<typeof AccordionPrimitive.Panel>) {
|
|
69
|
+
return (
|
|
70
|
+
<AccordionPrimitive.Panel
|
|
71
|
+
data-slot="accordion-content"
|
|
72
|
+
// Height animates off Base UI's own `--accordion-panel-height`, which it
|
|
73
|
+
// measures for us — `height: auto` is not animatable, so without the
|
|
74
|
+
// variable the panel would snap open.
|
|
75
|
+
className="h-(--accordion-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0 motion-reduce:transition-none"
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
<div className={cn('px-4 pb-4', className)}>{children}</div>
|
|
79
|
+
</AccordionPrimitive.Panel>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
|
package/src/ui/alert.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
|
|
6
|
+
const alertVariants = cva(
|
|
7
|
+
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-card text-card-foreground',
|
|
12
|
+
destructive:
|
|
13
|
+
'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
|
|
14
|
+
// Custom colours, per the shadcn recipe: a tinted fill with matching
|
|
15
|
+
// text rather than the card background, so an advisory note reads as
|
|
16
|
+
// information instead of as a card of its own. Mirrors the `success`
|
|
17
|
+
// recipe in badge.tsx, including the dark-mode text bump for legibility.
|
|
18
|
+
info: 'border-blue-500/20 bg-blue-500/10 text-blue-800 *:data-[slot=alert-description]:text-blue-800/90 dark:text-blue-300 dark:*:data-[slot=alert-description]:text-blue-300/90',
|
|
19
|
+
// Amber rather than the `--color-warning` token, which is yellow-500 —
|
|
20
|
+
// a fill colour too light to read as body text. Amber is what the rest
|
|
21
|
+
// of the dashboard already uses to mark a warning.
|
|
22
|
+
warning:
|
|
23
|
+
'border-amber-500/20 bg-amber-500/10 text-amber-800 *:data-[slot=alert-description]:text-amber-800/90 dark:text-amber-300 dark:*:data-[slot=alert-description]:text-amber-300/90',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
variant: 'default',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
function Alert({
|
|
33
|
+
className,
|
|
34
|
+
variant,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="alert"
|
|
40
|
+
role="alert"
|
|
41
|
+
className={cn(alertVariants({ variant }), className)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="alert-title"
|
|
51
|
+
className={cn(
|
|
52
|
+
'font-semibold group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
data-slot="alert-description"
|
|
64
|
+
className={cn(
|
|
65
|
+
'text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
74
|
+
return (
|
|
75
|
+
<div data-slot="alert-action" className={cn('absolute top-2 right-2', className)} {...props} />
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { Alert, AlertAction, AlertDescription, AlertTitle }
|
package/src/ui/attachment.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { Button } from './button'
|
|
|
12
12
|
// `cn-*` preset classes flattened into plain utilities (this package predates
|
|
13
13
|
// the shadcn CSS layer).
|
|
14
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-
|
|
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-accent/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
|
|
16
16
|
{
|
|
17
17
|
variants: {
|
|
18
18
|
size: {
|
package/src/ui/badge.tsx
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
-
import
|
|
3
|
-
import * as React from 'react'
|
|
2
|
+
import type * as React from 'react'
|
|
4
3
|
import { useTranslation } from 'react-i18next'
|
|
5
4
|
import { cn } from '../lib/utils'
|
|
6
5
|
import { Slot } from './slot'
|
|
7
6
|
|
|
8
7
|
const badgeVariants = cva(
|
|
9
|
-
|
|
8
|
+
// No `border-transparent` here: it and a variant's `border-*` have the same
|
|
9
|
+
// specificity, so the one Tailwind emits last wins regardless of the order
|
|
10
|
+
// they appear in the class list — which silently erased the outline
|
|
11
|
+
// variant's border. Each variant states its own border colour instead.
|
|
12
|
+
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] duration-100 ease-out focus-visible:border-ring focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] 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
13
|
{
|
|
11
14
|
variants: {
|
|
12
15
|
variant: {
|
|
13
|
-
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
14
|
-
secondary:
|
|
16
|
+
default: 'border-transparent bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
17
|
+
secondary:
|
|
18
|
+
'border-transparent bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
|
19
|
+
// The four status variants below share one recipe: a tint from the
|
|
20
|
+
// status ramp, text from the same ramp's reading step, and a border
|
|
21
|
+
// that firms the shape up on busy surfaces. Both themes get real
|
|
22
|
+
// colours rather than one hue at reduced opacity — an opacity tint
|
|
23
|
+
// takes on whatever sits behind it, which turns a badge muddy over a
|
|
24
|
+
// striped table row and washes it out entirely on a dark card. Linked
|
|
25
|
+
// badges darken to the border step on hover, which is the next stop up
|
|
26
|
+
// the same ramp rather than a second colour.
|
|
15
27
|
destructive:
|
|
16
|
-
'bg-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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',
|
|
28
|
+
'border-danger-border bg-danger-bg text-danger focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-danger-border',
|
|
29
|
+
success: 'border-success-border bg-success-bg text-success [a]:hover:bg-success-border',
|
|
30
|
+
info: 'border-info-border bg-info-bg text-info [a]:hover:bg-info-border',
|
|
31
|
+
warning: 'border-warning-border bg-warning-bg text-warning [a]:hover:bg-warning-border',
|
|
22
32
|
outline:
|
|
23
|
-
'border-border text-foreground/75 [a]:hover:bg-
|
|
24
|
-
ghost:
|
|
25
|
-
|
|
33
|
+
'border-border bg-card/50 text-foreground/75 [a]:hover:bg-accent [a]:hover:text-muted-foreground',
|
|
34
|
+
ghost:
|
|
35
|
+
'border-transparent hover:bg-accent hover:text-muted-foreground dark:hover:bg-accent/50',
|
|
36
|
+
link: 'border-transparent text-primary underline-offset-4 hover:underline',
|
|
26
37
|
},
|
|
27
38
|
},
|
|
28
39
|
defaultVariants: {
|
|
@@ -49,61 +60,193 @@ function Badge({
|
|
|
49
60
|
)
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Semantic tone of a status, independent of how it is drawn. `StatusBadge`
|
|
65
|
+
* renders it as a coloured dot, `Badge` as a tinted pill — same vocabulary,
|
|
66
|
+
* two presentations.
|
|
67
|
+
*/
|
|
68
|
+
type StatusTone = 'success' | 'warning' | 'destructive' | 'info' | 'neutral'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Maps an order/payment/product status code to its semantic tone.
|
|
72
|
+
*
|
|
73
|
+
* Kept as one table because the same codes surface in tables, cards, page
|
|
74
|
+
* headers and filter menus — a local copy in any one of those is how two
|
|
75
|
+
* surfaces end up disagreeing about what "pending" looks like. Extend it
|
|
76
|
+
* rather than branching on status at a call site.
|
|
77
|
+
*/
|
|
78
|
+
const statusToneMap: Record<string, StatusTone> = {
|
|
57
79
|
active: 'success',
|
|
80
|
+
authorized: 'info',
|
|
58
81
|
complete: 'success',
|
|
59
82
|
completed: 'success',
|
|
60
83
|
paid: 'success',
|
|
61
|
-
|
|
84
|
+
fulfilled: 'success',
|
|
85
|
+
delivered: 'success',
|
|
86
|
+
unfulfilled: 'warning',
|
|
87
|
+
proposed: 'warning',
|
|
62
88
|
published: 'success',
|
|
63
89
|
approved: 'success',
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
verified: 'success',
|
|
91
|
+
unverified: 'destructive',
|
|
92
|
+
unavailable: 'neutral',
|
|
93
|
+
unsupported: 'neutral',
|
|
94
|
+
revoked: 'destructive',
|
|
95
|
+
expired: 'destructive',
|
|
96
|
+
ready: 'warning',
|
|
97
|
+
available: 'success',
|
|
98
|
+
// Stock states, as the Inventory page's filter names them: sellable is
|
|
99
|
+
// good, nothing to sell is the problem, and the two middle states are units
|
|
100
|
+
// that exist but cannot be sold today — held by a checkout, or still on the
|
|
101
|
+
// road.
|
|
102
|
+
in_stock: 'success',
|
|
103
|
+
out_of_stock: 'destructive',
|
|
104
|
+
with_reserved: 'warning',
|
|
105
|
+
with_incoming: 'info',
|
|
106
|
+
draft: 'neutral',
|
|
107
|
+
pending: 'warning',
|
|
108
|
+
processing: 'warning',
|
|
109
|
+
inactive: 'neutral',
|
|
110
|
+
confirm: 'neutral',
|
|
111
|
+
resumed: 'neutral',
|
|
112
|
+
partial: 'info',
|
|
113
|
+
// What a carrier reports about a consignment in flight. `pending` and
|
|
114
|
+
// `delivered` are already above — they mean the same thing here.
|
|
115
|
+
pre_transit: 'info',
|
|
116
|
+
in_transit: 'info',
|
|
117
|
+
out_for_delivery: 'info',
|
|
118
|
+
available_for_pickup: 'warning',
|
|
119
|
+
return_to_sender: 'destructive',
|
|
120
|
+
failure: 'destructive',
|
|
121
|
+
unknown: 'neutral',
|
|
122
|
+
archived: 'neutral',
|
|
123
|
+
returned: 'success',
|
|
124
|
+
backorder: 'warning',
|
|
125
|
+
// Order payment status: amber while money is still owed either way, blue
|
|
126
|
+
// for a hold or a part refund, red once the authorisation is gone.
|
|
127
|
+
none: 'neutral',
|
|
128
|
+
partially_paid: 'warning',
|
|
129
|
+
partially_refunded: 'info',
|
|
130
|
+
overcharged: 'warning',
|
|
131
|
+
voided: 'destructive',
|
|
132
|
+
balance_due: 'warning',
|
|
133
|
+
credit_owed: 'warning',
|
|
81
134
|
canceled: 'destructive',
|
|
135
|
+
suspended: 'destructive',
|
|
136
|
+
invited: 'info',
|
|
137
|
+
onboarding: 'warning',
|
|
138
|
+
incomplete: 'neutral',
|
|
139
|
+
ready_for_review: 'warning',
|
|
82
140
|
failed: 'destructive',
|
|
83
141
|
void: 'destructive',
|
|
84
142
|
error: 'destructive',
|
|
85
143
|
rejected: 'destructive',
|
|
144
|
+
used_up: 'destructive',
|
|
145
|
+
// Returns, exchanges and claims: amber while the merchant still owes an
|
|
146
|
+
// action, green once settled, muted when it went nowhere.
|
|
147
|
+
requested: 'warning',
|
|
148
|
+
open: 'warning',
|
|
149
|
+
received: 'warning',
|
|
150
|
+
refunded: 'success',
|
|
151
|
+
resolved: 'success',
|
|
152
|
+
denied: 'destructive',
|
|
153
|
+
// Inventory operations. `received` is deliberately absent: on a return it
|
|
154
|
+
// means the merchant still owes a refund, on a transfer or purchase order it
|
|
155
|
+
// means the job is done — one code, two tones, so those two callers pass
|
|
156
|
+
// `tone` explicitly.
|
|
157
|
+
ready_to_ship: 'warning',
|
|
158
|
+
ordered: 'info',
|
|
159
|
+
partially_received: 'info',
|
|
160
|
+
// Gift cards.
|
|
161
|
+
partially_redeemed: 'info',
|
|
162
|
+
redeemed: 'neutral',
|
|
163
|
+
// Price lists — `scheduled` is live-but-not-yet, which is the same shape as
|
|
164
|
+
// any other "waiting on the clock" state.
|
|
165
|
+
scheduled: 'info',
|
|
166
|
+
// Boolean filter values. A yes/no pair is a state like any other — "in
|
|
167
|
+
// stock: no" is the row an operator is looking for, and reading it in the
|
|
168
|
+
// same green/red the rest of the app uses saves them parsing the word.
|
|
169
|
+
true: 'success',
|
|
170
|
+
false: 'destructive',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Dot fills, by tone. These read the same `--status-*` ramps the tinted Badge
|
|
174
|
+
// variants use, but take the *reading* step rather than the fill step: a 6px
|
|
175
|
+
// dot needs the saturated end of the ramp to register at all, where a pill can
|
|
176
|
+
// rely on area.
|
|
177
|
+
const dotToneClasses: Record<StatusTone, string> = {
|
|
178
|
+
success: 'bg-success',
|
|
179
|
+
warning: 'bg-warning',
|
|
180
|
+
destructive: 'bg-danger',
|
|
181
|
+
info: 'bg-info',
|
|
182
|
+
neutral: 'bg-muted-foreground/50',
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Bare status dot. Decorative by construction — it carries no accessible name,
|
|
187
|
+
* so it must always sit beside text that says the same thing. Use it to align
|
|
188
|
+
* a status with its label in a menu or list; use `StatusBadge` when you want
|
|
189
|
+
* the dot and its label together.
|
|
190
|
+
*/
|
|
191
|
+
function StatusDot({
|
|
192
|
+
status,
|
|
193
|
+
tone: toneOverride,
|
|
194
|
+
className,
|
|
195
|
+
}: {
|
|
196
|
+
status: string
|
|
197
|
+
tone?: StatusTone
|
|
198
|
+
className?: string
|
|
199
|
+
}) {
|
|
200
|
+
const tone = toneOverride ?? statusToneMap[status] ?? 'neutral'
|
|
201
|
+
return (
|
|
202
|
+
<span
|
|
203
|
+
aria-hidden
|
|
204
|
+
className={cn('inline-block size-2 shrink-0 rounded-full', dotToneClasses[tone], className)}
|
|
205
|
+
/>
|
|
206
|
+
)
|
|
86
207
|
}
|
|
87
208
|
|
|
88
209
|
/**
|
|
89
|
-
* Status
|
|
90
|
-
*
|
|
91
|
-
*
|
|
210
|
+
* Status indicator: a coloured dot beside the label, rather than a tinted pill.
|
|
211
|
+
*
|
|
212
|
+
* A table column of pills reads as a column of coloured blocks — the eye lands
|
|
213
|
+
* on the fills instead of the words, and a row where three statuses sit side by
|
|
214
|
+
* side becomes a bar chart of nothing. A dot puts the colour where it belongs:
|
|
215
|
+
* a small mark that the label does the talking for. Colour is never the only
|
|
216
|
+
* channel, since the label is always rendered.
|
|
217
|
+
*
|
|
218
|
+
* Stays headless: pass a translated `label` from the app layer; without one it
|
|
219
|
+
* humanizes the code itself (`balance_due` → `balance due`) as a best-effort
|
|
220
|
+
* fallback.
|
|
221
|
+
*
|
|
222
|
+
* `tone` overrides the shared map, for the rare code that means different
|
|
223
|
+
* things in two domains — a `received` return still owes a refund, a
|
|
224
|
+
* `received` stock transfer is finished. Reach for it only then: everything
|
|
225
|
+
* else belongs in `statusToneMap`, or two surfaces will disagree.
|
|
92
226
|
*/
|
|
93
227
|
function StatusBadge({
|
|
94
228
|
status,
|
|
95
229
|
label,
|
|
230
|
+
tone,
|
|
96
231
|
className,
|
|
97
232
|
}: {
|
|
98
233
|
status: string
|
|
99
234
|
label?: string
|
|
235
|
+
tone?: StatusTone
|
|
100
236
|
className?: string
|
|
101
237
|
}) {
|
|
102
|
-
const variant = statusVariantMap[status] ?? 'outline'
|
|
103
238
|
return (
|
|
104
|
-
<
|
|
239
|
+
<span
|
|
240
|
+
data-slot="status-badge"
|
|
241
|
+
data-status={status}
|
|
242
|
+
className={cn(
|
|
243
|
+
'inline-flex w-fit shrink-0 items-center gap-1.5 whitespace-nowrap text-sm capitalize',
|
|
244
|
+
className,
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<StatusDot status={status} tone={tone} />
|
|
105
248
|
{label ?? status.replace(/_/g, ' ')}
|
|
106
|
-
</
|
|
249
|
+
</span>
|
|
107
250
|
)
|
|
108
251
|
}
|
|
109
252
|
|
|
@@ -140,22 +283,21 @@ function ActiveBadge({
|
|
|
140
283
|
const { t } = useTranslation()
|
|
141
284
|
const resolvedActiveLabel = activeLabel ?? t('admin.common.yes')
|
|
142
285
|
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) {
|
|
286
|
+
if (dashWhenInactive && !active) {
|
|
152
287
|
return <span className={cn('text-muted-foreground', className)}>—</span>
|
|
153
288
|
}
|
|
289
|
+
// Boolean states read through the same map as every other status, so an
|
|
290
|
+
// "Active / Inactive" column and a "Paid / Failed" one agree on what green
|
|
291
|
+
// and red mean. The tick the active case used to carry is redundant beside
|
|
292
|
+
// a dot that already says the same thing.
|
|
154
293
|
return (
|
|
155
|
-
<
|
|
156
|
-
{
|
|
157
|
-
|
|
294
|
+
<StatusBadge
|
|
295
|
+
status={active ? 'true' : 'false'}
|
|
296
|
+
label={active ? resolvedActiveLabel : resolvedInactiveLabel}
|
|
297
|
+
className={className}
|
|
298
|
+
/>
|
|
158
299
|
)
|
|
159
300
|
}
|
|
160
301
|
|
|
161
|
-
export {
|
|
302
|
+
export type { StatusTone }
|
|
303
|
+
export { ActiveBadge, Badge, badgeVariants, StatusBadge, StatusDot, statusToneMap }
|
package/src/ui/breadcrumb.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import i18n from 'i18next'
|
|
2
|
-
import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
|
|
3
2
|
import type * as React from 'react'
|
|
4
3
|
import { cn } from '../lib/utils'
|
|
4
|
+
import { ChevronRightIcon, MoreHorizontalIcon } from '../spree/icons'
|
|
5
5
|
import { Slot } from './slot'
|
|
6
6
|
|
|
7
7
|
function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
|
package/src/ui/button.tsx
CHANGED
|
@@ -4,26 +4,38 @@ import { cn } from '../lib/utils'
|
|
|
4
4
|
import { Slot } from './slot'
|
|
5
5
|
|
|
6
6
|
const buttonVariants = cva(
|
|
7
|
-
"group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-
|
|
7
|
+
"group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-lg border border-transparent text-sm font-medium whitespace-nowrap cursor-pointer select-none no-underline transition-[color,background-color,border-color,box-shadow,transform,translate,scale] duration-100 ease-out outline-none active:scale-[0.98] motion-reduce:active:scale-100 focus-visible:border-blue-500 focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] 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
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// `shadow-xs` like the outline and destructive variants: the primary
|
|
12
|
+
// action is the one thing on a page that should sit slightly above it,
|
|
13
|
+
// and it was the only variant with no lift at all.
|
|
14
|
+
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/85',
|
|
13
15
|
outline:
|
|
14
|
-
'border-border bg-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
'border-border bg-card shadow-xs text-foreground hover:bg-accent hover:text-foreground aria-expanded:bg-accent',
|
|
17
|
+
// `group-hover/row:` — inside a hovered table row the row already wears
|
|
18
|
+
// --accent, so repeating it here leaves the button at 1.03:1 against its
|
|
19
|
+
// own background: invisible exactly when the pointer is on it. On a
|
|
20
|
+
// hovered row it steps to --accent-strong instead.
|
|
21
|
+
ghost:
|
|
22
|
+
'hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent group-hover/row:hover:bg-accent-strong',
|
|
23
|
+
destructive: 'text-destructive bg-card border-border shadow-xs hover:bg-destructive/10',
|
|
24
|
+
// Ghost's chrome (none until hover) with destructive's colour. The
|
|
25
|
+
// inline remove buttons that sit inside a row or field want no box of
|
|
26
|
+
// their own, but still need the red tint on hover rather than ghost's
|
|
27
|
+
// neutral grey — several had been reproducing exactly this by hand.
|
|
28
|
+
'destructive-ghost': 'text-destructive hover:bg-destructive/10 hover:text-destructive',
|
|
17
29
|
link: 'text-primary underline-offset-4 hover:underline',
|
|
18
30
|
},
|
|
19
31
|
size: {
|
|
20
32
|
default: 'h-9 px-3 py-1.5',
|
|
21
|
-
xs: "h-6 gap-1 rounded-
|
|
22
|
-
sm: "h-7 gap-1 rounded-
|
|
33
|
+
xs: "h-6 gap-1 rounded-md px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
|
|
34
|
+
sm: "h-7 gap-1 rounded-md px-2 py-1 text-sm [&_svg:not([class*='size-'])]:size-3.5",
|
|
23
35
|
lg: 'h-10 px-3 py-2 text-base',
|
|
24
36
|
icon: 'size-9',
|
|
25
|
-
'icon-xs': "size-6 rounded-
|
|
26
|
-
'icon-sm': 'size-7 rounded-
|
|
37
|
+
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
38
|
+
'icon-sm': 'size-7 rounded-md',
|
|
27
39
|
'icon-lg': 'size-10',
|
|
28
40
|
},
|
|
29
41
|
},
|
package/src/ui/calendar.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
|
|
2
1
|
import type * as React from 'react'
|
|
3
2
|
import { DayPicker } from 'react-day-picker'
|
|
4
3
|
import { activeDateLocale } from '../lib/date-locale'
|
|
5
4
|
import { cn } from '../lib/utils'
|
|
5
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '../spree/icons'
|
|
6
6
|
|
|
7
7
|
function Calendar({
|
|
8
8
|
className,
|
package/src/ui/card.tsx
CHANGED
|
@@ -2,12 +2,50 @@ import type * as React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { cn } from '../lib/utils'
|
|
4
4
|
|
|
5
|
-
function Card({
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
variant = 'default',
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<'div'> & {
|
|
10
|
+
/**
|
|
11
|
+
* `nested` is one resource listed inside a card — a fulfillment on an order,
|
|
12
|
+
* a return, a rule. It sits on a recessed surface so it reads as a thing
|
|
13
|
+
* *within* the card rather than another card beside it, and carries its own
|
|
14
|
+
* horizontal padding so its sections divide edge to edge.
|
|
15
|
+
*
|
|
16
|
+
* The box is load-bearing once there is more than one of them: a fulfillment
|
|
17
|
+
* already divides its own rows, so grouping by rules alone would draw the
|
|
18
|
+
* boundary between two fulfillments as the same line at the same inset as
|
|
19
|
+
* the boundary between two items — and the panel's own actions would have no
|
|
20
|
+
* visible owner.
|
|
21
|
+
*
|
|
22
|
+
* `container` is the card those nested records sit in. Pair the two: the
|
|
23
|
+
* container recedes, the records stay raised. A `default` card wrapping
|
|
24
|
+
* `nested` ones puts the content in a well and the frame in front of it,
|
|
25
|
+
* which reads as the content being demoted.
|
|
26
|
+
*/
|
|
27
|
+
variant?: 'default' | 'nested' | 'container'
|
|
28
|
+
}) {
|
|
6
29
|
return (
|
|
7
30
|
<div
|
|
8
31
|
data-slot="card"
|
|
32
|
+
data-variant={variant}
|
|
9
33
|
className={cn(
|
|
10
|
-
|
|
34
|
+
// `overflow-x-clip`: a wide child (a table sized to its columns) otherwise
|
|
35
|
+
// propagates its width up through every `overflow: visible` ancestor to
|
|
36
|
+
// the document, laying the whole page out wider than the viewport. `clip`
|
|
37
|
+
// rather than `hidden` so `overflow-y` stays `visible` and sticky
|
|
38
|
+
// descendants keep resolving against the page.
|
|
39
|
+
'group/card flex flex-col min-w-0 overflow-x-clip break-words text-card-foreground shadow-xs',
|
|
40
|
+
variant === 'nested' &&
|
|
41
|
+
'rounded-lg border border-border-subtle bg-nested-raised px-3 shadow-none',
|
|
42
|
+
// Holds nested records rather than content, so it recedes and lets
|
|
43
|
+
// them be the raised, readable thing.
|
|
44
|
+
variant === 'container' && 'rounded-xl border border-border-card bg-card-container',
|
|
45
|
+
// `--border-card`, not `--border`: a card is a region of the sheet
|
|
46
|
+
// rather than a surface floating above it, so its edge is softer than
|
|
47
|
+
// the structural one the sheet, dialogs and dropdowns draw.
|
|
48
|
+
variant === 'default' && 'rounded-xl border border-border-card bg-card',
|
|
11
49
|
className,
|
|
12
50
|
)}
|
|
13
51
|
{...props}
|
|
@@ -20,7 +58,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
20
58
|
<div
|
|
21
59
|
data-slot="card-header"
|
|
22
60
|
className={cn(
|
|
23
|
-
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] px-3 py-2.5 border-b border-border rounded-t-[calc(var(--radius-2xl)-1px)]',
|
|
61
|
+
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] px-3 py-2.5 border-b border-border-subtle rounded-t-[calc(var(--radius-2xl)-1px)] group-data-[variant=nested]/card:rounded-none group-data-[variant=nested]/card:px-0 group-data-[variant=nested]/card:py-2 group-data-[variant=nested]/card:items-center',
|
|
24
62
|
className,
|
|
25
63
|
)}
|
|
26
64
|
{...props}
|
|
@@ -32,7 +70,9 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
32
70
|
return (
|
|
33
71
|
<div
|
|
34
72
|
data-slot="card-title"
|
|
35
|
-
|
|
73
|
+
// Same size and weight as SheetTitle and DialogTitle: a card, a sheet
|
|
74
|
+
// and a dialog heading are the same rank of thing.
|
|
75
|
+
className={cn('text-base font-semibold flex items-center gap-2', className)}
|
|
36
76
|
{...props}
|
|
37
77
|
/>
|
|
38
78
|
)
|
|
@@ -53,7 +93,13 @@ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
53
93
|
}
|
|
54
94
|
|
|
55
95
|
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
56
|
-
return
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
data-slot="card-content"
|
|
99
|
+
className={cn('p-3 group-data-[variant=nested]/card:px-0', className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
57
103
|
}
|
|
58
104
|
|
|
59
105
|
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
@@ -61,7 +107,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
61
107
|
<div
|
|
62
108
|
data-slot="card-footer"
|
|
63
109
|
className={cn(
|
|
64
|
-
'flex items-center p-3 bg-transparent border-t border-border rounded-b-[calc(var(--radius-xl)-1px)]',
|
|
110
|
+
'flex items-center p-3 bg-transparent border-t border-border-subtle rounded-b-[calc(var(--radius-xl)-1px)] group-data-[variant=nested]/card:rounded-none group-data-[variant=nested]/card:px-0',
|
|
65
111
|
className,
|
|
66
112
|
)}
|
|
67
113
|
{...props}
|