@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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/package.json +15 -9
  3. package/src/hooks/use-prefers-reduced-motion.ts +26 -0
  4. package/src/hooks/use-scrolled.ts +34 -14
  5. package/src/index.ts +39 -1
  6. package/src/lib/date-range-presets.ts +79 -0
  7. package/src/lib/same-rich-text.ts +145 -0
  8. package/src/spree/address-block.tsx +5 -4
  9. package/src/spree/address-book-row.tsx +96 -0
  10. package/src/spree/address-map.tsx +62 -0
  11. package/src/spree/back-button.tsx +14 -9
  12. package/src/spree/bulk-dialog.tsx +2 -2
  13. package/src/spree/bulk-price-table.tsx +145 -14
  14. package/src/spree/bulk-variants-table.tsx +434 -0
  15. package/src/spree/calculator-summary.tsx +5 -1
  16. package/src/spree/claim-resolve-dialog.tsx +219 -0
  17. package/src/spree/confirm-dialog.tsx +1 -1
  18. package/src/spree/copy-to-clipboard-button.tsx +1 -1
  19. package/src/spree/data-grid/cells.tsx +199 -25
  20. package/src/spree/data-grid/context.ts +6 -0
  21. package/src/spree/data-grid/data-grid.tsx +149 -7
  22. package/src/spree/data-grid/helpers.ts +6 -2
  23. package/src/spree/data-grid/index.ts +9 -1
  24. package/src/spree/data-grid/use-data-grid-keyboard.ts +4 -2
  25. package/src/spree/delivery-form-dialog.tsx +166 -0
  26. package/src/spree/drag-handle.tsx +2 -2
  27. package/src/spree/editable-note-card.tsx +103 -0
  28. package/src/spree/external-link.tsx +56 -0
  29. package/src/spree/file-type-icon.tsx +68 -0
  30. package/src/spree/form-actions.tsx +2 -3
  31. package/src/spree/form-section.tsx +46 -0
  32. package/src/spree/fulfill-items-form.tsx +282 -0
  33. package/src/spree/fulfillment-edit-dialog.tsx +211 -0
  34. package/src/spree/fulfillment-item-list.tsx +61 -0
  35. package/src/spree/fulfillment-panel.tsx +53 -0
  36. package/src/spree/icons.ts +167 -0
  37. package/src/spree/json-preview-drawer.tsx +4 -4
  38. package/src/spree/label-upload-dialog.tsx +195 -0
  39. package/src/spree/language-menu-items.tsx +37 -30
  40. package/src/spree/metadata/metadata-card.tsx +1 -1
  41. package/src/spree/order-cancel-dialog.tsx +149 -0
  42. package/src/spree/post-sale-create-dialogs.tsx +253 -0
  43. package/src/spree/post-sale-fields.tsx +133 -0
  44. package/src/spree/product-membership-list.tsx +453 -0
  45. package/src/spree/products/status-card.tsx +79 -0
  46. package/src/spree/quantity-tier-editor.tsx +162 -0
  47. package/src/spree/resource-combobox.tsx +12 -1
  48. package/src/spree/resource-name-cell.tsx +21 -4
  49. package/src/spree/return-dialogs.tsx +238 -0
  50. package/src/spree/route-error-boundary.tsx +1 -1
  51. package/src/spree/row-actions.tsx +2 -2
  52. package/src/spree/search-input.tsx +76 -0
  53. package/src/spree/secret-input.tsx +26 -40
  54. package/src/spree/tax-identifiers-card.tsx +418 -0
  55. package/src/spree/theme-toggle.tsx +36 -23
  56. package/src/spree/wizard.tsx +254 -0
  57. package/src/styles.css +750 -75
  58. package/src/ui/accordion.tsx +83 -0
  59. package/src/ui/alert.tsx +79 -0
  60. package/src/ui/attachment.tsx +1 -1
  61. package/src/ui/badge.tsx +198 -56
  62. package/src/ui/breadcrumb.tsx +1 -1
  63. package/src/ui/button.tsx +22 -10
  64. package/src/ui/calendar.tsx +1 -1
  65. package/src/ui/card.tsx +52 -6
  66. package/src/ui/checkbox.tsx +13 -5
  67. package/src/ui/combobox.tsx +125 -6
  68. package/src/ui/command.tsx +149 -23
  69. package/src/ui/context-menu.tsx +99 -0
  70. package/src/ui/data-table.tsx +448 -9
  71. package/src/ui/date-picker.tsx +18 -3
  72. package/src/ui/date-range-picker.tsx +47 -60
  73. package/src/ui/dialog.tsx +6 -6
  74. package/src/ui/dropdown-menu.tsx +18 -11
  75. package/src/ui/field.tsx +10 -2
  76. package/src/ui/input-group.tsx +1 -1
  77. package/src/ui/input.tsx +1 -1
  78. package/src/ui/map.tsx +2110 -0
  79. package/src/ui/media-preview.tsx +105 -0
  80. package/src/ui/pagination.tsx +19 -11
  81. package/src/ui/popover.tsx +4 -2
  82. package/src/ui/progress.tsx +5 -2
  83. package/src/ui/radio-group.tsx +4 -4
  84. package/src/ui/rich-text-editor.tsx +214 -88
  85. package/src/ui/scroll-area.tsx +65 -0
  86. package/src/ui/select.tsx +17 -10
  87. package/src/ui/separator.tsx +1 -1
  88. package/src/ui/sheet.tsx +49 -7
  89. package/src/ui/sidebar.tsx +138 -37
  90. package/src/ui/skeleton.tsx +3 -1
  91. package/src/ui/tabs.tsx +84 -0
  92. package/src/ui/textarea.tsx +1 -1
  93. package/src/ui/thumbnail.tsx +80 -0
  94. package/src/ui/toast.tsx +112 -0
  95. package/src/ui/tooltip.tsx +3 -1
  96. package/src/ui/sonner.tsx +0 -47
@@ -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 }
@@ -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-lg border border-border bg-card px-2.5 py-1.5 text-base font-normal leading-normal text-foreground shadow-xs transition-all duration-100 ease-in-out outline-none placeholder:text-muted-foreground focus:border-blue-500 focus:shadow-[0_0_0_3px_rgba(59,130,246,0.15)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:border-border disabled:text-muted-foreground disabled:shadow-none aria-invalid:border-destructive',
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 }
@@ -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
+ }
@@ -85,7 +85,9 @@ function TooltipContent({
85
85
  <TooltipPrimitive.Popup
86
86
  data-slot="tooltip-content"
87
87
  className={cn(
88
- '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 data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform] duration-100',
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 }