@spree/dashboard-ui 1.0.0-rc.2 → 1.0.0-rc.4

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.
@@ -0,0 +1,188 @@
1
+ import { mergeProps } from '@base-ui/react/merge-props'
2
+ import { useRender } from '@base-ui/react/use-render'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import * as React from 'react'
5
+ import { cn } from '../lib/utils'
6
+
7
+ import { Separator } from './separator'
8
+
9
+ function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
10
+ return (
11
+ <div
12
+ role="list"
13
+ data-slot="item-group"
14
+ className={cn(
15
+ 'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
24
+ return (
25
+ <Separator
26
+ data-slot="item-separator"
27
+ orientation="horizontal"
28
+ className={cn('my-2', className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ const itemVariants = cva(
35
+ 'group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted',
36
+ {
37
+ variants: {
38
+ variant: {
39
+ default: 'border-transparent',
40
+ outline: 'border-border',
41
+ muted: 'border-transparent bg-muted/50',
42
+ },
43
+ size: {
44
+ default: 'gap-2.5 px-3 py-2.5',
45
+ sm: 'gap-2.5 px-3 py-2.5',
46
+ xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
47
+ },
48
+ },
49
+ defaultVariants: {
50
+ variant: 'default',
51
+ size: 'default',
52
+ },
53
+ },
54
+ )
55
+
56
+ function Item({
57
+ className,
58
+ variant = 'default',
59
+ size = 'default',
60
+ render,
61
+ ...props
62
+ }: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
63
+ return useRender({
64
+ defaultTagName: 'div',
65
+ props: mergeProps<'div'>(
66
+ {
67
+ className: cn(itemVariants({ variant, size, className })),
68
+ },
69
+ props,
70
+ ),
71
+ render,
72
+ state: {
73
+ slot: 'item',
74
+ variant,
75
+ size,
76
+ },
77
+ })
78
+ }
79
+
80
+ const itemMediaVariants = cva(
81
+ 'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
82
+ {
83
+ variants: {
84
+ variant: {
85
+ default: 'bg-transparent',
86
+ icon: "[&_svg:not([class*='size-'])]:size-4",
87
+ image:
88
+ 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
89
+ },
90
+ },
91
+ defaultVariants: {
92
+ variant: 'default',
93
+ },
94
+ },
95
+ )
96
+
97
+ function ItemMedia({
98
+ className,
99
+ variant = 'default',
100
+ ...props
101
+ }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
102
+ return (
103
+ <div
104
+ data-slot="item-media"
105
+ data-variant={variant}
106
+ className={cn(itemMediaVariants({ variant, className }))}
107
+ {...props}
108
+ />
109
+ )
110
+ }
111
+
112
+ function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
113
+ return (
114
+ <div
115
+ data-slot="item-content"
116
+ className={cn(
117
+ 'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
118
+ className,
119
+ )}
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
126
+ return (
127
+ <div
128
+ data-slot="item-title"
129
+ className={cn(
130
+ 'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
131
+ className,
132
+ )}
133
+ {...props}
134
+ />
135
+ )
136
+ }
137
+
138
+ function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
139
+ return (
140
+ <p
141
+ data-slot="item-description"
142
+ className={cn(
143
+ 'line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
144
+ className,
145
+ )}
146
+ {...props}
147
+ />
148
+ )
149
+ }
150
+
151
+ function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
152
+ return (
153
+ <div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
154
+ )
155
+ }
156
+
157
+ function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
158
+ return (
159
+ <div
160
+ data-slot="item-header"
161
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
162
+ {...props}
163
+ />
164
+ )
165
+ }
166
+
167
+ function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
168
+ return (
169
+ <div
170
+ data-slot="item-footer"
171
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
172
+ {...props}
173
+ />
174
+ )
175
+ }
176
+
177
+ export {
178
+ Item,
179
+ ItemActions,
180
+ ItemContent,
181
+ ItemDescription,
182
+ ItemFooter,
183
+ ItemGroup,
184
+ ItemHeader,
185
+ ItemMedia,
186
+ ItemSeparator,
187
+ ItemTitle,
188
+ }
package/src/ui/toast.tsx CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  TriangleAlertIcon,
13
13
  XIcon,
14
14
  } from '../spree/icons'
15
+ import { Button } from './button'
15
16
 
16
17
  /**
17
18
  * App-wide toast manager. Created outside React so a toast can be raised from
@@ -26,18 +27,89 @@ import {
26
27
  export const toastManager = ToastPrimitive.createToastManager()
27
28
 
28
29
  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" />,
30
+ success: <CircleCheckIcon className="text-emerald-600 dark:text-emerald-400" />,
31
+ error: <OctagonXIcon className="text-destructive" />,
32
+ warning: <TriangleAlertIcon className="text-amber-600 dark:text-amber-400" />,
33
+ info: <InfoIcon className="text-muted-foreground" />,
34
+ loading: <Loader2Icon className="animate-spin text-muted-foreground" />,
35
+ }
36
+
37
+ function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
38
+ return (
39
+ <ToastPrimitive.Viewport
40
+ data-slot="toast-viewport"
41
+ className={cn(
42
+ // `z-40`, below the overlay layer (`z-50`) rather than above
43
+ // everything: the stack sits bottom-right, which is exactly where a
44
+ // side sheet's footer buttons are, and a toast drifting over Save is
45
+ // both unclickable and a click the merchant did not intend.
46
+ 'pointer-events-none fixed inset-x-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-40 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full',
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
55
+ return (
56
+ <ToastPrimitive.Root
57
+ data-slot="toast"
58
+ className={cn(
59
+ 'group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
60
+ '[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]',
61
+ 'h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]',
62
+ "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
63
+ 'data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]',
64
+ 'data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]',
65
+ '[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]',
66
+ 'data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
67
+ 'data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
68
+ 'data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
69
+ 'data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
70
+ 'data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]',
71
+ 'data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]',
72
+ 'data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]',
73
+ 'data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]',
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
82
+ return (
83
+ <ToastPrimitive.Content
84
+ data-slot="toast-content"
85
+ className={cn(
86
+ 'flex items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100',
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function ToastIcon({ type }: { type: string | undefined }) {
95
+ const icon = type ? ICONS[type] : null
96
+ if (!icon) return null
97
+
98
+ return (
99
+ <span
100
+ data-slot="toast-icon"
101
+ className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
102
+ >
103
+ {icon}
104
+ </span>
105
+ )
34
106
  }
35
107
 
36
108
  function ToastList() {
37
109
  const { toasts } = ToastPrimitive.useToastManager()
38
110
 
39
111
  return toasts.map((toast) => (
40
- <ToastPrimitive.Root
112
+ <Toast
41
113
  key={toast.id}
42
114
  toast={toast}
43
115
  // Base UI types a toast as `dialog`, which puts every transient
@@ -45,37 +117,31 @@ function ToastList() {
45
117
  // lookup, ours or a screen reader user's, matches whatever toast happens
46
118
  // to be on screen. `status` is what a passive notification should expose.
47
119
  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
120
  >
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
- )}
121
+ <ToastContent>
122
+ <ToastIcon type={toast.type} />
65
123
  <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" />
124
+ <ToastPrimitive.Title data-slot="toast-title" className="font-medium text-sm" />
125
+ <ToastPrimitive.Description
126
+ data-slot="toast-description"
127
+ className="text-muted-foreground text-sm"
128
+ />
68
129
  </div>
130
+ <ToastPrimitive.Action
131
+ data-slot="toast-action"
132
+ render={<Button variant="outline" size="sm" />}
133
+ className="shrink-0"
134
+ />
69
135
  <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"
136
+ data-slot="toast-close"
73
137
  aria-label={i18n.t('admin.actions.close')}
138
+ render={<Button variant="ghost" size="icon-sm" />}
139
+ className="relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground"
74
140
  >
75
- <XIcon className="size-4" />
141
+ <XIcon aria-hidden="true" />
76
142
  </ToastPrimitive.Close>
77
- </div>
78
- </ToastPrimitive.Root>
143
+ </ToastContent>
144
+ </Toast>
79
145
  ))
80
146
  }
81
147
 
@@ -90,22 +156,10 @@ export function Toaster({ children }: { children?: React.ReactNode }) {
90
156
  return (
91
157
  <ToastPrimitive.Provider toastManager={toastManager}>
92
158
  {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
- >
159
+ <ToastPrimitive.Portal data-slot="toast-portal">
160
+ <ToastViewport>
107
161
  <ToastList />
108
- </ToastPrimitive.Viewport>
162
+ </ToastViewport>
109
163
  </ToastPrimitive.Portal>
110
164
  </ToastPrimitive.Provider>
111
165
  )