@spree/dashboard-ui 0.13.1 → 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
package/src/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
function Tabs({
|
|
6
|
+
className,
|
|
7
|
+
orientation = 'horizontal',
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
10
|
+
return (
|
|
11
|
+
<TabsPrimitive.Root
|
|
12
|
+
data-slot="tabs"
|
|
13
|
+
orientation={orientation}
|
|
14
|
+
className={cn(
|
|
15
|
+
'group/tabs flex gap-2 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:flex-row',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The track a set of tabs sits in, recessed so the selected tab reads as raised. */
|
|
24
|
+
function TabsList({
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
|
29
|
+
return (
|
|
30
|
+
<TabsPrimitive.List
|
|
31
|
+
data-slot="tabs-list"
|
|
32
|
+
className={cn(
|
|
33
|
+
'inline-flex w-fit items-center justify-center gap-1 rounded-lg bg-track-recessed p-1 text-muted-foreground',
|
|
34
|
+
'group-data-[orientation=vertical]/tabs:h-auto group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-col',
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</TabsPrimitive.List>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Tab>) {
|
|
45
|
+
return (
|
|
46
|
+
<TabsPrimitive.Tab
|
|
47
|
+
data-slot="tabs-trigger"
|
|
48
|
+
className={cn(
|
|
49
|
+
// min-h-7 + py-1 keeps the hit area past the 24px WCAG target size at
|
|
50
|
+
// every text size; the old h-[calc(100%-1px)] left it to the parent.
|
|
51
|
+
'inline-flex min-h-7 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-md px-2.5 py-1 text-sm font-medium whitespace-nowrap',
|
|
52
|
+
'transition-[color,background-color,box-shadow] duration-150 ease-out outline-none motion-reduce:transition-none',
|
|
53
|
+
'group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start',
|
|
54
|
+
// Unselected tabs recede so the selected one reads as chosen; hover
|
|
55
|
+
// promotes to full foreground rather than only shifting the surface,
|
|
56
|
+
// which is the part a colour-blind or low-vision reader can see.
|
|
57
|
+
'text-muted-foreground hover:text-foreground',
|
|
58
|
+
// The raised surface lives on the tab itself, as it did before the
|
|
59
|
+
// indicator: it is the thing that makes the selection legible, and it
|
|
60
|
+
// must not depend on a separate element rendering correctly behind it.
|
|
61
|
+
'data-[active]:bg-nested-raised data-[active]:text-foreground data-[active]:shadow-xs',
|
|
62
|
+
// Same focus treatment as Button, so a keyboard user sees one
|
|
63
|
+
// consistent indicator across the dashboard.
|
|
64
|
+
'focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]',
|
|
65
|
+
'disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed',
|
|
66
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Panel>) {
|
|
75
|
+
return (
|
|
76
|
+
<TabsPrimitive.Panel
|
|
77
|
+
data-slot="tabs-content"
|
|
78
|
+
className={cn('flex-1 outline-none', className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
package/src/ui/textarea.tsx
CHANGED
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
|
7
7
|
<textarea
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
|
-
'flex field-sizing-content min-h-16 w-full rounded-
|
|
10
|
+
'flex field-sizing-content min-h-16 w-full rounded-md border border-border bg-card px-2.5 py-1.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none placeholder:text-muted-foreground focus:border-blue-500 focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground aria-invalid:border-destructive',
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import type * as React from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
import { ImageIcon } from '../spree/icons'
|
|
5
|
+
|
|
6
|
+
const thumbnailVariants = cva(
|
|
7
|
+
// The wrapper always renders, so a record without an image keeps the same
|
|
8
|
+
// footprint as one with it and rows never reflow. `shrink-0` matters in the
|
|
9
|
+
// flex rows these sit in — without it a long product name squashes the image.
|
|
10
|
+
'flex shrink-0 items-center justify-center overflow-hidden border bg-muted text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*=size-])]:size-4',
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
xs: 'size-5 [&_svg:not([class*=size-])]:size-3',
|
|
15
|
+
sm: 'size-8',
|
|
16
|
+
md: 'size-10',
|
|
17
|
+
lg: 'size-12 [&_svg:not([class*=size-])]:size-5',
|
|
18
|
+
},
|
|
19
|
+
shape: {
|
|
20
|
+
rounded: 'rounded-md',
|
|
21
|
+
square: 'rounded-none',
|
|
22
|
+
circle: 'rounded-full',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
size: 'md',
|
|
27
|
+
shape: 'rounded',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
export interface ThumbnailProps
|
|
33
|
+
extends Omit<React.ComponentProps<'div'>, 'children'>,
|
|
34
|
+
VariantProps<typeof thumbnailVariants> {
|
|
35
|
+
/** Image to show. When absent or failed, the fallback renders instead. */
|
|
36
|
+
src?: string | null
|
|
37
|
+
/**
|
|
38
|
+
* Describes the image for screen readers. Leave unset when a visible label
|
|
39
|
+
* sits beside the thumbnail — the image is decorative then, and repeating
|
|
40
|
+
* the name makes screen readers announce it twice.
|
|
41
|
+
*/
|
|
42
|
+
alt?: string
|
|
43
|
+
/** Overrides the placeholder shown when there is no image. */
|
|
44
|
+
fallback?: React.ReactNode
|
|
45
|
+
/** Defer loading until the thumbnail is near the viewport. */
|
|
46
|
+
loading?: 'eager' | 'lazy'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A product or record image at a fixed size, falling back to a placeholder.
|
|
51
|
+
*
|
|
52
|
+
* Images are cropped to fill the box (`object-cover`), which suits photography.
|
|
53
|
+
* Logos and artwork that must not be cropped want `Attachment` instead.
|
|
54
|
+
*/
|
|
55
|
+
export function Thumbnail({
|
|
56
|
+
src,
|
|
57
|
+
alt = '',
|
|
58
|
+
fallback,
|
|
59
|
+
size,
|
|
60
|
+
shape,
|
|
61
|
+
className,
|
|
62
|
+
loading,
|
|
63
|
+
...props
|
|
64
|
+
}: ThumbnailProps) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="thumbnail"
|
|
68
|
+
className={cn(thumbnailVariants({ size, shape }), className)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{src ? (
|
|
72
|
+
<img src={src} alt={alt} loading={loading} className="size-full object-cover" />
|
|
73
|
+
) : (
|
|
74
|
+
(fallback ?? <ImageIcon />)
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { thumbnailVariants }
|
package/src/ui/toast.tsx
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Toast as ToastPrimitive } from '@base-ui/react/toast'
|
|
4
|
+
import i18n from 'i18next'
|
|
5
|
+
import type * as React from 'react'
|
|
6
|
+
import { cn } from '../lib/utils'
|
|
7
|
+
import {
|
|
8
|
+
CircleCheckIcon,
|
|
9
|
+
InfoIcon,
|
|
10
|
+
Loader2Icon,
|
|
11
|
+
OctagonXIcon,
|
|
12
|
+
TriangleAlertIcon,
|
|
13
|
+
XIcon,
|
|
14
|
+
} from '../spree/icons'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* App-wide toast manager. Created outside React so a toast can be raised from
|
|
18
|
+
* anywhere — a mutation hook, an event subscriber, a plain async function —
|
|
19
|
+
* without threading a hook through the call stack.
|
|
20
|
+
*
|
|
21
|
+
* Raise one with `toastManager.add({ type: 'success', title: '…' })`. Passing
|
|
22
|
+
* an `id` that already exists updates that toast in place and restarts its
|
|
23
|
+
* timer, which is how a long-running job reports progress through a single
|
|
24
|
+
* toast rather than stacking one per step.
|
|
25
|
+
*/
|
|
26
|
+
export const toastManager = ToastPrimitive.createToastManager()
|
|
27
|
+
|
|
28
|
+
const ICONS: Record<string, React.ReactNode> = {
|
|
29
|
+
success: <CircleCheckIcon className="size-4 text-emerald-600 dark:text-emerald-400" />,
|
|
30
|
+
error: <OctagonXIcon className="size-4 text-destructive" />,
|
|
31
|
+
warning: <TriangleAlertIcon className="size-4 text-amber-600 dark:text-amber-400" />,
|
|
32
|
+
info: <InfoIcon className="size-4 text-muted-foreground" />,
|
|
33
|
+
loading: <Loader2Icon className="size-4 animate-spin text-muted-foreground" />,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ToastList() {
|
|
37
|
+
const { toasts } = ToastPrimitive.useToastManager()
|
|
38
|
+
|
|
39
|
+
return toasts.map((toast) => (
|
|
40
|
+
<ToastPrimitive.Root
|
|
41
|
+
key={toast.id}
|
|
42
|
+
toast={toast}
|
|
43
|
+
// Base UI types a toast as `dialog`, which puts every transient
|
|
44
|
+
// notification in the same bucket as real modals — a `getByRole('dialog')`
|
|
45
|
+
// lookup, ours or a screen reader user's, matches whatever toast happens
|
|
46
|
+
// to be on screen. `status` is what a passive notification should expose.
|
|
47
|
+
role={toast.type === 'error' ? 'alert' : 'status'}
|
|
48
|
+
// `--toast-index` / `--toast-offset-y` drive the stacked-card effect:
|
|
49
|
+
// toasts behind the front one scale down and sit slightly higher.
|
|
50
|
+
className={cn(
|
|
51
|
+
// Normal flow inside a bottom-anchored column, not absolute
|
|
52
|
+
// positioning: a zero-height viewport gives an absolutely-positioned
|
|
53
|
+
// child nothing to sit on, and it renders below the fold.
|
|
54
|
+
'pointer-events-auto w-full',
|
|
55
|
+
'rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-lg',
|
|
56
|
+
'transition-all duration-200 ease-out',
|
|
57
|
+
'data-[starting-style]:translate-y-2 data-[starting-style]:opacity-0',
|
|
58
|
+
'data-[ending-style]:translate-y-2 data-[ending-style]:opacity-0',
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<div className="flex items-start gap-3">
|
|
62
|
+
{toast.type && ICONS[toast.type] && (
|
|
63
|
+
<span className="mt-0.5 shrink-0">{ICONS[toast.type]}</span>
|
|
64
|
+
)}
|
|
65
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
66
|
+
<ToastPrimitive.Title className="font-medium text-sm leading-snug" />
|
|
67
|
+
<ToastPrimitive.Description className="text-muted-foreground text-sm leading-snug" />
|
|
68
|
+
</div>
|
|
69
|
+
<ToastPrimitive.Close
|
|
70
|
+
// 44px tap target on touch, compact for a pointer — the same rule the
|
|
71
|
+
// rest of the app follows for icon-only controls.
|
|
72
|
+
className="-mt-1 -mr-1 inline-flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
73
|
+
aria-label={i18n.t('admin.actions.close')}
|
|
74
|
+
>
|
|
75
|
+
<XIcon className="size-4" />
|
|
76
|
+
</ToastPrimitive.Close>
|
|
77
|
+
</div>
|
|
78
|
+
</ToastPrimitive.Root>
|
|
79
|
+
))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Mount once at the app root, inside the provider that owns the queue.
|
|
84
|
+
*
|
|
85
|
+
* Replaces the Sonner toaster: shadcn dropped that integration in favour of
|
|
86
|
+
* this Base UI primitive, which is also what every other overlay in this app
|
|
87
|
+
* (Sheet, Dialog, Popover, Select) is already built on.
|
|
88
|
+
*/
|
|
89
|
+
export function Toaster({ children }: { children?: React.ReactNode }) {
|
|
90
|
+
return (
|
|
91
|
+
<ToastPrimitive.Provider toastManager={toastManager}>
|
|
92
|
+
{children}
|
|
93
|
+
<ToastPrimitive.Portal>
|
|
94
|
+
<ToastPrimitive.Viewport
|
|
95
|
+
className={cn(
|
|
96
|
+
// Below the overlay layer (`z-50`) rather than above everything:
|
|
97
|
+
// the stack sits bottom-right, which is exactly where a side
|
|
98
|
+
// sheet's footer buttons are, and a toast drifting over Save is
|
|
99
|
+
// both unclickable and a click the merchant did not intend.
|
|
100
|
+
'fixed right-4 z-40 flex w-[min(22rem,calc(100vw-2rem))] flex-col gap-2',
|
|
101
|
+
// `pointer-events-none` on the column so the empty space above the
|
|
102
|
+
// stack never blocks the page; each toast takes them back.
|
|
103
|
+
'pointer-events-none',
|
|
104
|
+
'bottom-[max(1rem,env(safe-area-inset-bottom))]',
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<ToastList />
|
|
108
|
+
</ToastPrimitive.Viewport>
|
|
109
|
+
</ToastPrimitive.Portal>
|
|
110
|
+
</ToastPrimitive.Provider>
|
|
111
|
+
)
|
|
112
|
+
}
|
package/src/ui/tooltip.tsx
CHANGED
|
@@ -85,7 +85,9 @@ function TooltipContent({
|
|
|
85
85
|
<TooltipPrimitive.Popup
|
|
86
86
|
data-slot="tooltip-content"
|
|
87
87
|
className={cn(
|
|
88
|
-
|
|
88
|
+
// `origin-(--transform-origin)` so it grows out of the element it
|
|
89
|
+
// describes rather than its own centre.
|
|
90
|
+
'z-50 inline-flex w-fit max-w-[200px] items-center gap-1.5 rounded-lg border border-border bg-popover px-2 py-1 text-sm font-normal text-popover-foreground shadow-md origin-(--transform-origin) data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform,translate,scale] duration-100 ease-out',
|
|
89
91
|
className,
|
|
90
92
|
)}
|
|
91
93
|
{...props}
|
package/src/ui/sonner.tsx
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CircleCheckIcon,
|
|
3
|
-
InfoIcon,
|
|
4
|
-
Loader2Icon,
|
|
5
|
-
OctagonXIcon,
|
|
6
|
-
TriangleAlertIcon,
|
|
7
|
-
} from 'lucide-react'
|
|
8
|
-
import type { CSSProperties } from 'react'
|
|
9
|
-
import { Toaster as Sonner, type ToasterProps } from 'sonner'
|
|
10
|
-
import { useTheme } from '../spree/theme-provider'
|
|
11
|
-
|
|
12
|
-
const Toaster = ({ ...props }: ToasterProps) => {
|
|
13
|
-
// The shadcn-default Sonner template assumes `next-themes`. This app has its
|
|
14
|
-
// own ThemeProvider that exposes a `mode` (the user's selection: light/dark/system)
|
|
15
|
-
// — Sonner's `theme` prop accepts the same three values, so we forward it.
|
|
16
|
-
const { mode } = useTheme()
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<Sonner
|
|
20
|
-
theme={mode as ToasterProps['theme']}
|
|
21
|
-
className="toaster group"
|
|
22
|
-
icons={{
|
|
23
|
-
success: <CircleCheckIcon className="size-4" />,
|
|
24
|
-
info: <InfoIcon className="size-4" />,
|
|
25
|
-
warning: <TriangleAlertIcon className="size-4" />,
|
|
26
|
-
error: <OctagonXIcon className="size-4" />,
|
|
27
|
-
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
28
|
-
}}
|
|
29
|
-
style={
|
|
30
|
-
{
|
|
31
|
-
'--normal-bg': 'var(--popover)',
|
|
32
|
-
'--normal-text': 'var(--popover-foreground)',
|
|
33
|
-
'--normal-border': 'var(--border)',
|
|
34
|
-
'--border-radius': 'var(--radius)',
|
|
35
|
-
} as CSSProperties
|
|
36
|
-
}
|
|
37
|
-
toastOptions={{
|
|
38
|
-
classNames: {
|
|
39
|
-
toast: 'cn-toast',
|
|
40
|
-
},
|
|
41
|
-
}}
|
|
42
|
-
{...props}
|
|
43
|
-
/>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export { Toaster }
|