@spree/dashboard-ui 6.0.3 → 6.0.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.
- package/package.json +20 -19
- package/src/index.ts +5 -0
- package/src/lib/field-trigger.ts +10 -0
- package/src/lib/same-rich-text.ts +12 -3
- package/src/lib/utils.ts +14 -1
- package/src/spree/address-block.tsx +3 -3
- package/src/spree/address-book-row.tsx +88 -0
- package/src/spree/bulk-price-table.tsx +3 -3
- package/src/spree/claim-resolve-dialog.tsx +1 -1
- package/src/spree/collapsible-card.tsx +2 -2
- package/src/spree/color-picker.tsx +1 -1
- package/src/spree/currency-label.tsx +71 -0
- package/src/spree/data-grid/cells.tsx +2 -2
- package/src/spree/data-grid/data-grid.tsx +2 -2
- package/src/spree/delivery-form-dialog.tsx +1 -1
- package/src/spree/drag-handle.tsx +1 -1
- package/src/spree/editable-note-card.tsx +2 -2
- package/src/spree/form-actions.tsx +32 -10
- package/src/spree/fulfill-items-form.tsx +20 -22
- package/src/spree/fulfillment-edit-dialog.tsx +1 -1
- package/src/spree/fulfillment-item-list.tsx +87 -28
- package/src/spree/fulfillment-panel.tsx +2 -2
- package/src/spree/icons.ts +8 -0
- package/src/spree/json-preview-drawer.tsx +32 -7
- package/src/spree/json-value-view.tsx +14 -4
- package/src/spree/label-upload-dialog.tsx +1 -1
- package/src/spree/language-menu-items.tsx +1 -1
- package/src/spree/metadata/metadata-card.tsx +2 -2
- package/src/spree/order-cancel-dialog.tsx +3 -3
- package/src/spree/post-sale-create-dialogs.tsx +36 -33
- package/src/spree/post-sale-fields.tsx +30 -1
- package/src/spree/product-identity.tsx +74 -0
- package/src/spree/product-membership-list.tsx +79 -41
- package/src/spree/products/status-card.tsx +1 -1
- package/src/spree/quantity-tier-editor.tsx +1 -1
- package/src/spree/return-dialogs.tsx +2 -2
- package/src/spree/search-input.tsx +3 -1
- package/src/spree/secret-input.tsx +1 -1
- package/src/spree/section.tsx +55 -0
- package/src/spree/stat-item.tsx +27 -0
- package/src/spree/tax-identifiers-card.tsx +117 -76
- package/src/spree/theme-toggle.tsx +1 -1
- package/src/spree/wizard.tsx +2 -2
- package/src/styles.css +226 -120
- package/src/ui/alert.tsx +20 -17
- package/src/ui/attachment.tsx +1 -1
- package/src/ui/avatar.tsx +2 -2
- package/src/ui/badge.tsx +1 -1
- package/src/ui/breadcrumb.tsx +1 -1
- package/src/ui/button.tsx +14 -11
- package/src/ui/calendar.tsx +2 -2
- package/src/ui/card.tsx +14 -37
- package/src/ui/chart.tsx +1 -1
- package/src/ui/checkbox.tsx +2 -2
- package/src/ui/code-editor.tsx +6 -2
- package/src/ui/collapsible.tsx +21 -2
- package/src/ui/combobox.tsx +12 -16
- package/src/ui/command.tsx +8 -6
- package/src/ui/context-menu.tsx +1 -1
- package/src/ui/data-table.tsx +58 -15
- package/src/ui/date-picker.tsx +3 -3
- package/src/ui/date-range-panel.tsx +102 -0
- package/src/ui/date-range-picker.tsx +33 -72
- package/src/ui/dialog.tsx +3 -3
- package/src/ui/dropdown-menu.tsx +6 -6
- package/src/ui/empty.tsx +3 -3
- package/src/ui/field.tsx +33 -12
- package/src/ui/input-group.tsx +17 -6
- package/src/ui/input.tsx +148 -4
- package/src/ui/item.tsx +7 -4
- package/src/ui/label.tsx +1 -1
- package/src/ui/media-preview.tsx +40 -5
- package/src/ui/pagination.tsx +16 -4
- package/src/ui/progress.tsx +2 -2
- package/src/ui/radio-group.tsx +2 -2
- package/src/ui/rich-text-editor.tsx +3 -3
- package/src/ui/select.tsx +7 -16
- package/src/ui/sheet.tsx +13 -5
- package/src/ui/sidebar.tsx +5 -5
- package/src/ui/switch.tsx +8 -2
- package/src/ui/tabs.tsx +45 -9
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/toast.tsx +2 -2
- package/src/ui/tooltip.tsx +1 -1
package/src/ui/input-group.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { cn } from '../lib/utils'
|
|
4
4
|
import { Button } from './button'
|
|
5
|
-
import { Input } from './input'
|
|
5
|
+
import { Input, NumberInput } from './input'
|
|
6
6
|
import { Textarea } from './textarea'
|
|
7
7
|
|
|
8
8
|
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
@@ -11,7 +11,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
11
11
|
data-slot="input-group"
|
|
12
12
|
role="group"
|
|
13
13
|
className={cn(
|
|
14
|
-
'group/input-group relative flex w-full min-w-0 items-center rounded-
|
|
14
|
+
'group/input-group relative flex w-full min-w-0 items-center rounded-xl border border-field-border shadow-control bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none not-has-disabled:not-focus-within:hover:bg-field-hover has-disabled:bg-muted has-disabled:cursor-not-allowed has-disabled:text-muted-foreground has-[[data-slot=input-group-control]:focus-visible]:border-blue-500 has-[[data-slot=input-group-control]:focus-visible]:shadow-field-focus has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
|
|
15
15
|
className,
|
|
16
16
|
)}
|
|
17
17
|
{...props}
|
|
@@ -20,7 +20,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const inputGroupAddonVariants = cva(
|
|
23
|
-
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-
|
|
23
|
+
"flex h-auto cursor-default group-focus-within/input-group:cursor-text items-center justify-center gap-2 py-1.5 text-base font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
24
24
|
{
|
|
25
25
|
variants: {
|
|
26
26
|
align: {
|
|
@@ -60,7 +60,7 @@ function InputGroupAddon({
|
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const inputGroupButtonVariants = cva('flex items-center gap-2 text-
|
|
63
|
+
const inputGroupButtonVariants = cva('flex items-center gap-2 text-base shadow-none', {
|
|
64
64
|
variants: {
|
|
65
65
|
size: {
|
|
66
66
|
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
|
@@ -97,7 +97,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|
|
97
97
|
return (
|
|
98
98
|
<span
|
|
99
99
|
className={cn(
|
|
100
|
-
"flex items-center gap-2 text-
|
|
100
|
+
"flex items-center gap-2 text-base text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
101
101
|
className,
|
|
102
102
|
)}
|
|
103
103
|
{...props}
|
|
@@ -106,11 +106,22 @@ function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
|
|
109
|
+
if (props.type === 'number') {
|
|
110
|
+
return (
|
|
111
|
+
<NumberInput
|
|
112
|
+
bare
|
|
113
|
+
data-slot="input-group-control"
|
|
114
|
+
className={cn('flex-1 self-stretch', className)}
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
return (
|
|
110
121
|
<Input
|
|
111
122
|
data-slot="input-group-control"
|
|
112
123
|
className={cn(
|
|
113
|
-
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus:border-0 focus:shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0',
|
|
124
|
+
'flex-1 rounded-none border-0 bg-transparent not-disabled:not-read-only:not-focus:hover:bg-transparent shadow-none ring-0 focus:border-0 focus:shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0',
|
|
114
125
|
className,
|
|
115
126
|
)}
|
|
116
127
|
{...props}
|
package/src/ui/input.tsx
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from 'react'
|
|
2
2
|
|
|
3
3
|
import { cn } from '../lib/utils'
|
|
4
|
+
import { ChevronDownIcon, ChevronUpIcon } from '../spree/icons'
|
|
5
|
+
|
|
6
|
+
const FIELD_BOX =
|
|
7
|
+
'rounded-xl border border-field-border shadow-control bg-card transition-[color,background-color,border-color,box-shadow] duration-100 ease-out'
|
|
8
|
+
|
|
9
|
+
function Input({
|
|
10
|
+
className,
|
|
11
|
+
type,
|
|
12
|
+
stepper,
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<'input'> & {
|
|
15
|
+
/** `false` keeps a number field plain, where a column has no room for the buttons. */
|
|
16
|
+
stepper?: boolean
|
|
17
|
+
}) {
|
|
18
|
+
if (type === 'number' && stepper !== false)
|
|
19
|
+
return <NumberInput className={className} {...props} />
|
|
4
20
|
|
|
5
|
-
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
6
21
|
return (
|
|
7
22
|
<input
|
|
8
23
|
type={type}
|
|
9
24
|
data-slot="input"
|
|
10
25
|
className={cn(
|
|
11
|
-
|
|
26
|
+
FIELD_BOX,
|
|
27
|
+
'block w-full min-w-0 min-h-8 cursor-default focus:cursor-text py-1.5 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none px-2.5 text-base font-normal leading-normal text-foreground outline-none placeholder:text-muted-foreground not-disabled:not-read-only:not-focus:hover:bg-field-hover focus:border-blue-500 focus:shadow-field-focus disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground aria-invalid:border-destructive file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-base file:font-medium file:text-foreground',
|
|
12
28
|
className,
|
|
13
29
|
)}
|
|
14
30
|
{...props}
|
|
@@ -16,4 +32,132 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
|
16
32
|
)
|
|
17
33
|
}
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
// The two buttons stack into one pill 3px inside the field's edge. Its outer
|
|
36
|
+
// corners follow the field's (radius-xl, less the 1px border and the 3px gap),
|
|
37
|
+
// so the curves stay parallel; the corners where the buttons meet stay tight.
|
|
38
|
+
const STEP_RADIUS_UP = 'rounded-t-[calc(var(--radius-xl)-4px)] rounded-b-[2px]'
|
|
39
|
+
const STEP_RADIUS_DOWN = 'rounded-b-[calc(var(--radius-xl)-4px)] rounded-t-[2px]'
|
|
40
|
+
|
|
41
|
+
type NumberInputProps = Omit<React.ComponentProps<'input'>, 'type'> & {
|
|
42
|
+
/** Drops the field's own box, for an input that sits inside an `InputGroup`. */
|
|
43
|
+
bare?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A number field with increase and decrease buttons. Every `<Input
|
|
48
|
+
* type="number">` renders this, so forms get it without opting in.
|
|
49
|
+
*
|
|
50
|
+
* `className` lands on the outer box, so width, margin and border classes
|
|
51
|
+
* work as they do on a plain input; text alignment and numeral style carry
|
|
52
|
+
* through to the value. The buttons are skipped by Tab — the arrow keys
|
|
53
|
+
* already step the value.
|
|
54
|
+
*/
|
|
55
|
+
function NumberInput({ className, bare, disabled, readOnly, ref, ...props }: NumberInputProps) {
|
|
56
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
57
|
+
// A form library passes its own ref; the buttons need the element too.
|
|
58
|
+
const setRef = (node: HTMLInputElement | null) => {
|
|
59
|
+
inputRef.current = node
|
|
60
|
+
if (typeof ref === 'function') ref(node)
|
|
61
|
+
else if (ref) ref.current = node
|
|
62
|
+
}
|
|
63
|
+
const repeat = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
64
|
+
const interactive = !disabled && !readOnly
|
|
65
|
+
|
|
66
|
+
const step = (direction: 1 | -1) => {
|
|
67
|
+
const input = inputRef.current
|
|
68
|
+
if (!input) return
|
|
69
|
+
try {
|
|
70
|
+
direction === 1 ? input.stepUp() : input.stepDown()
|
|
71
|
+
} catch {
|
|
72
|
+
// `step="any"` has no step to take; move by one, inside min and max.
|
|
73
|
+
let next = (Number.parseFloat(input.value) || 0) + direction
|
|
74
|
+
if (input.min !== '') next = Math.max(next, Number(input.min))
|
|
75
|
+
if (input.max !== '') next = Math.min(next, Number(input.max))
|
|
76
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(
|
|
77
|
+
input,
|
|
78
|
+
String(next),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
// The value changed outside React, so announce it the way typing would.
|
|
82
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
83
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stopRepeat = () => {
|
|
87
|
+
if (repeat.current) clearTimeout(repeat.current)
|
|
88
|
+
repeat.current = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Holding a button keeps stepping, slowly at first.
|
|
92
|
+
const startRepeat = (direction: 1 | -1) => (event: React.PointerEvent) => {
|
|
93
|
+
if (event.button !== 0) return
|
|
94
|
+
event.preventDefault()
|
|
95
|
+
inputRef.current?.focus()
|
|
96
|
+
step(direction)
|
|
97
|
+
const tick = (delay: number) => {
|
|
98
|
+
repeat.current = setTimeout(() => {
|
|
99
|
+
step(direction)
|
|
100
|
+
tick(60)
|
|
101
|
+
}, delay)
|
|
102
|
+
}
|
|
103
|
+
tick(400)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => stopRepeat, [])
|
|
107
|
+
|
|
108
|
+
const button = (direction: 1 | -1) => (
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
tabIndex={-1}
|
|
112
|
+
aria-hidden
|
|
113
|
+
disabled={!interactive}
|
|
114
|
+
onPointerDown={startRepeat(direction)}
|
|
115
|
+
onPointerUp={stopRepeat}
|
|
116
|
+
onPointerLeave={stopRepeat}
|
|
117
|
+
onPointerCancel={stopRepeat}
|
|
118
|
+
className={cn(
|
|
119
|
+
'flex flex-1 cursor-pointer items-center justify-center bg-stepper text-muted-foreground transition-colors hover:bg-stepper-hover hover:text-foreground active:bg-stepper-active active:text-foreground disabled:cursor-not-allowed [&_svg]:size-3',
|
|
120
|
+
direction === 1 ? STEP_RADIUS_UP : STEP_RADIUS_DOWN,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{direction === 1 ? (
|
|
124
|
+
<ChevronUpIcon strokeWidth={2.5} />
|
|
125
|
+
) : (
|
|
126
|
+
<ChevronDownIcon strokeWidth={2.5} />
|
|
127
|
+
)}
|
|
128
|
+
</button>
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<span
|
|
133
|
+
data-slot="number-field"
|
|
134
|
+
className={cn(
|
|
135
|
+
'relative flex w-full min-w-0 items-stretch text-foreground',
|
|
136
|
+
!bare &&
|
|
137
|
+
cn(
|
|
138
|
+
FIELD_BOX,
|
|
139
|
+
'min-h-8 not-has-[input:disabled]:not-has-[input:read-only]:not-focus-within:hover:bg-field-hover focus-within:border-blue-500 focus-within:shadow-field-focus has-[input:disabled]:cursor-not-allowed has-[input:disabled]:bg-muted has-[input:disabled]:text-muted-foreground has-aria-invalid:border-destructive',
|
|
140
|
+
),
|
|
141
|
+
className,
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
<input
|
|
145
|
+
ref={setRef}
|
|
146
|
+
type="number"
|
|
147
|
+
data-slot="input"
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
readOnly={readOnly}
|
|
150
|
+
{...props}
|
|
151
|
+
className="block w-full min-w-0 flex-1 cursor-default focus:cursor-text appearance-none bg-transparent py-1.5 pl-2.5 pr-1 text-base font-normal leading-normal text-inherit [font-variant-numeric:inherit] [text-align:inherit] outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
152
|
+
/>
|
|
153
|
+
{interactive && (
|
|
154
|
+
<span className="flex w-6 shrink-0 flex-col gap-px py-[3px] pr-[3px]">
|
|
155
|
+
{button(1)}
|
|
156
|
+
{button(-1)}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</span>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { Input, NumberInput }
|
package/src/ui/item.tsx
CHANGED
|
@@ -32,12 +32,13 @@ function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Sepa
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const itemVariants = cva(
|
|
35
|
-
'group/item flex w-full flex-wrap items-center rounded-lg border text-
|
|
35
|
+
'group/item flex w-full flex-wrap items-center rounded-lg border text-base 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
36
|
{
|
|
37
37
|
variants: {
|
|
38
38
|
variant: {
|
|
39
39
|
default: 'border-transparent',
|
|
40
|
-
|
|
40
|
+
// A card in small: its corner, faint border, fill and soft lift.
|
|
41
|
+
outline: 'rounded-card border-card-edge bg-card shadow-card-lift',
|
|
41
42
|
muted: 'border-transparent bg-muted/50',
|
|
42
43
|
},
|
|
43
44
|
size: {
|
|
@@ -46,6 +47,8 @@ const itemVariants = cva(
|
|
|
46
47
|
xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
|
|
47
48
|
},
|
|
48
49
|
},
|
|
50
|
+
// An outline item takes a card's 14px inset, so the two line up.
|
|
51
|
+
compoundVariants: [{ variant: 'outline', size: 'default', className: 'px-3.5 py-3' }],
|
|
49
52
|
defaultVariants: {
|
|
50
53
|
variant: 'default',
|
|
51
54
|
size: 'default',
|
|
@@ -127,7 +130,7 @@ function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
127
130
|
<div
|
|
128
131
|
data-slot="item-title"
|
|
129
132
|
className={cn(
|
|
130
|
-
'line-clamp-1 flex w-fit items-center gap-2 text-
|
|
133
|
+
'line-clamp-1 flex w-fit items-center gap-2 text-base font-medium underline-offset-4',
|
|
131
134
|
className,
|
|
132
135
|
)}
|
|
133
136
|
{...props}
|
|
@@ -140,7 +143,7 @@ function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
|
140
143
|
<p
|
|
141
144
|
data-slot="item-description"
|
|
142
145
|
className={cn(
|
|
143
|
-
'line-clamp-2 text-left text-
|
|
146
|
+
'line-clamp-2 text-left text-base font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
|
|
144
147
|
className,
|
|
145
148
|
)}
|
|
146
149
|
{...props}
|
package/src/ui/label.tsx
CHANGED
|
@@ -15,7 +15,7 @@ function Label({ className, children, ...props }: React.ComponentProps<'label'>)
|
|
|
15
15
|
<Field.Label
|
|
16
16
|
data-slot="label"
|
|
17
17
|
className={cn(
|
|
18
|
-
'flex items-center gap-2 text-
|
|
18
|
+
'flex items-center gap-2 text-base font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
19
19
|
className,
|
|
20
20
|
)}
|
|
21
21
|
{...props}
|
package/src/ui/media-preview.tsx
CHANGED
|
@@ -21,6 +21,12 @@ export interface MediaPreviewProps {
|
|
|
21
21
|
/** Click handler for setting the focal point; images only. */
|
|
22
22
|
onFocalPointClick?: (event: React.MouseEvent<HTMLElement>) => void
|
|
23
23
|
className?: string
|
|
24
|
+
/**
|
|
25
|
+
* `width` fills its container's width (a panel); `stage` shrinks the frame
|
|
26
|
+
* to the picture and fits it within the viewport, for a full-screen editor —
|
|
27
|
+
* so a focal point click always lands on the image, never on letterboxing.
|
|
28
|
+
*/
|
|
29
|
+
fit?: 'width' | 'stage'
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -41,14 +47,23 @@ export function MediaPreview({
|
|
|
41
47
|
focalPoint,
|
|
42
48
|
onFocalPointClick,
|
|
43
49
|
className = 'max-h-[60vh]',
|
|
50
|
+
fit = 'width',
|
|
44
51
|
}: MediaPreviewProps) {
|
|
45
52
|
const { t } = useTranslation()
|
|
46
53
|
|
|
47
54
|
const isImage = mediaType === 'image'
|
|
55
|
+
const stage = fit === 'stage'
|
|
56
|
+
// The stage's own padding and the editor's top bar, so the picture fits.
|
|
57
|
+
const stageHeight = 'max-h-[calc(100dvh-9rem)]'
|
|
48
58
|
|
|
49
59
|
if (mediaType === 'external_video' && embedUrl) {
|
|
50
60
|
return (
|
|
51
|
-
<div
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
'aspect-video w-full shrink-0 overflow-hidden rounded-lg border border-border bg-black',
|
|
64
|
+
stage && 'max-w-5xl',
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
52
67
|
<iframe
|
|
53
68
|
src={embedUrl}
|
|
54
69
|
title={alt || t('admin.components.media_preview.video_title')}
|
|
@@ -68,7 +83,10 @@ export function MediaPreview({
|
|
|
68
83
|
controls
|
|
69
84
|
playsInline
|
|
70
85
|
preload="metadata"
|
|
71
|
-
className={cn(
|
|
86
|
+
className={cn(
|
|
87
|
+
'shrink-0 rounded-lg bg-black',
|
|
88
|
+
stage ? cn('max-w-full', stageHeight) : cn('w-full', className),
|
|
89
|
+
)}
|
|
72
90
|
/>
|
|
73
91
|
)
|
|
74
92
|
}
|
|
@@ -80,11 +98,23 @@ export function MediaPreview({
|
|
|
80
98
|
type="button"
|
|
81
99
|
disabled={!canSetFocalPoint}
|
|
82
100
|
onClick={onFocalPointClick}
|
|
83
|
-
className=
|
|
101
|
+
className={cn(
|
|
102
|
+
'group relative shrink-0 overflow-hidden rounded-lg border border-border bg-muted disabled:cursor-default',
|
|
103
|
+
stage ? 'inline-block max-w-full border-0 bg-transparent' : 'block w-full',
|
|
104
|
+
canSetFocalPoint && 'cursor-crosshair',
|
|
105
|
+
)}
|
|
84
106
|
>
|
|
85
107
|
{previewUrl ? (
|
|
86
108
|
<>
|
|
87
|
-
<img
|
|
109
|
+
<img
|
|
110
|
+
src={previewUrl}
|
|
111
|
+
alt={alt}
|
|
112
|
+
className={
|
|
113
|
+
stage
|
|
114
|
+
? cn('block h-auto w-auto max-w-full object-contain', stageHeight)
|
|
115
|
+
: cn('w-full object-contain', className)
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
88
118
|
{isImage && focalPoint && (
|
|
89
119
|
<span
|
|
90
120
|
aria-hidden
|
|
@@ -96,7 +126,12 @@ export function MediaPreview({
|
|
|
96
126
|
) : (
|
|
97
127
|
// A video with no poster has no still to show; say which kind of file
|
|
98
128
|
// it is rather than rendering a broken image.
|
|
99
|
-
<div
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
'flex aspect-square items-center justify-center text-muted-foreground',
|
|
132
|
+
stage ? 'w-64' : 'w-full',
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
100
135
|
{isImage ? <ImagePlusIcon className="size-8" /> : <FilmIcon className="size-8" />}
|
|
101
136
|
</div>
|
|
102
137
|
)}
|
package/src/ui/pagination.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next'
|
|
2
|
+
import { cn } from '../lib/utils'
|
|
2
3
|
import {
|
|
3
4
|
ChevronLeftIcon,
|
|
4
5
|
ChevronRightIcon,
|
|
@@ -24,6 +25,11 @@ interface PaginationProps {
|
|
|
24
25
|
onPageChange: (page: number) => void
|
|
25
26
|
onPageSizeChange?: (size: number) => void
|
|
26
27
|
pageSizeOptions?: number[]
|
|
28
|
+
/**
|
|
29
|
+
* For the foot of a card whose table runs edge to edge: a divider above and
|
|
30
|
+
* padding on every side, so the range and buttons don't touch the card edge.
|
|
31
|
+
*/
|
|
32
|
+
inset?: boolean
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function Pagination({
|
|
@@ -31,6 +37,7 @@ export function Pagination({
|
|
|
31
37
|
onPageChange,
|
|
32
38
|
onPageSizeChange,
|
|
33
39
|
pageSizeOptions = [10, 20, 25, 30, 50],
|
|
40
|
+
inset = false,
|
|
34
41
|
}: PaginationProps) {
|
|
35
42
|
const { t } = useTranslation()
|
|
36
43
|
const showPageSize = Boolean(onPageSizeChange) && meta.count > 0
|
|
@@ -39,8 +46,13 @@ export function Pagination({
|
|
|
39
46
|
if (!showPageSize && !showPageNav) return null
|
|
40
47
|
|
|
41
48
|
return (
|
|
42
|
-
<div
|
|
43
|
-
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
'flex items-center justify-between',
|
|
52
|
+
inset ? 'border-border-subtle border-t px-4 py-2.5' : 'ps-2 py-2',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<div className="whitespace-nowrap text-base text-muted-foreground">
|
|
44
56
|
{t('admin.components.pagination.range', {
|
|
45
57
|
from: meta.from,
|
|
46
58
|
to: meta.to,
|
|
@@ -50,7 +62,7 @@ export function Pagination({
|
|
|
50
62
|
<div className="flex items-center gap-2 sm:gap-6">
|
|
51
63
|
{showPageSize && (
|
|
52
64
|
<div className="hidden items-center gap-2 sm:flex">
|
|
53
|
-
<span className="text-
|
|
65
|
+
<span className="text-base text-muted-foreground">
|
|
54
66
|
{t('admin.components.pagination.rows_per_page')}
|
|
55
67
|
</span>
|
|
56
68
|
<Select
|
|
@@ -72,7 +84,7 @@ export function Pagination({
|
|
|
72
84
|
)}
|
|
73
85
|
{showPageNav && (
|
|
74
86
|
<>
|
|
75
|
-
<span className="hidden whitespace-nowrap text-
|
|
87
|
+
<span className="hidden whitespace-nowrap text-base text-muted-foreground sm:inline">
|
|
76
88
|
{t('admin.common.page_of', { page: meta.page, total: meta.pages })}
|
|
77
89
|
</span>
|
|
78
90
|
<div className="flex items-center gap-1">
|
package/src/ui/progress.tsx
CHANGED
|
@@ -57,7 +57,7 @@ function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
|
|
57
57
|
return (
|
|
58
58
|
<ProgressPrimitive.Label
|
|
59
59
|
data-slot="progress-label"
|
|
60
|
-
className={cn('font-medium text-
|
|
60
|
+
className={cn('font-medium text-base', className)}
|
|
61
61
|
{...props}
|
|
62
62
|
/>
|
|
63
63
|
)
|
|
@@ -67,7 +67,7 @@ function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
|
|
67
67
|
return (
|
|
68
68
|
<ProgressPrimitive.Value
|
|
69
69
|
data-slot="progress-value"
|
|
70
|
-
className={cn('ml-auto text-muted-foreground text-
|
|
70
|
+
className={cn('ml-auto text-muted-foreground text-base', className)}
|
|
71
71
|
{...props}
|
|
72
72
|
/>
|
|
73
73
|
)
|
package/src/ui/radio-group.tsx
CHANGED
|
@@ -18,8 +18,8 @@ function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof Rad
|
|
|
18
18
|
<RadioPrimitive.Root
|
|
19
19
|
data-slot="radio-group-item"
|
|
20
20
|
className={cn(
|
|
21
|
-
'peer relative inline-block size-4 shrink-0 cursor-pointer rounded-full border border-border bg-card align-[-3px] outline-none transition-[color,background-color,border-color,box-shadow] duration-100 ease-out',
|
|
22
|
-
'focus:border-blue-500 focus:shadow-
|
|
21
|
+
'peer relative inline-block size-4 shrink-0 cursor-pointer rounded-full border border-field-border shadow-control bg-card align-[-3px] outline-none transition-[color,background-color,border-color,box-shadow] duration-100 ease-out',
|
|
22
|
+
'focus:border-blue-500 focus:shadow-field-focus',
|
|
23
23
|
'disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground',
|
|
24
24
|
'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
|
|
25
25
|
'data-[checked]:border-blue-500 data-[checked]:bg-blue-500 data-[checked]:text-white',
|
|
@@ -191,7 +191,7 @@ export function RichTextEditor({
|
|
|
191
191
|
// in a form beside them without reading as a different kind of control.
|
|
192
192
|
// `focus-within` rather than `focus`: the element that takes the caret
|
|
193
193
|
// is Tiptap's inner contenteditable, not this wrapper.
|
|
194
|
-
'rounded-
|
|
194
|
+
'rounded-xl border border-field-border shadow-control bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out focus-within:border-blue-500 focus-within:shadow-field-focus',
|
|
195
195
|
disabled && 'pointer-events-none bg-muted text-muted-foreground shadow-none',
|
|
196
196
|
className,
|
|
197
197
|
)}
|
|
@@ -252,7 +252,7 @@ function EditorToolbar({
|
|
|
252
252
|
return (
|
|
253
253
|
<div
|
|
254
254
|
data-slot="rich-text-editor-toolbar"
|
|
255
|
-
className="flex items-center gap-0.5
|
|
255
|
+
className="flex items-center gap-0.5 rounded-t-[inherit] bg-secondary px-2 py-1.5"
|
|
256
256
|
>
|
|
257
257
|
<ToolbarButton
|
|
258
258
|
active={toolbar.isBold}
|
|
@@ -375,7 +375,7 @@ function ToolbarButton({
|
|
|
375
375
|
aria-label={title}
|
|
376
376
|
aria-pressed={active}
|
|
377
377
|
className={cn(
|
|
378
|
-
'inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-40 disabled:pointer-events-none',
|
|
378
|
+
'inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground hover:bg-accent-strong-hover hover:text-foreground transition-colors disabled:opacity-40 disabled:pointer-events-none',
|
|
379
379
|
active && 'bg-accent-strong text-foreground',
|
|
380
380
|
)}
|
|
381
381
|
>
|
package/src/ui/select.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Select as SelectPrimitive } from '@base-ui/react/select'
|
|
2
2
|
import type * as React from 'react'
|
|
3
|
+
import { fieldTriggerClassName } from '../lib/field-trigger'
|
|
3
4
|
import { cn } from '../lib/utils'
|
|
4
|
-
import { CheckIcon, ChevronDownIcon } from '../spree/icons'
|
|
5
|
+
import { CheckIcon, ChevronDownIcon, ChevronsUpDownIcon } from '../spree/icons'
|
|
5
6
|
|
|
6
7
|
function Select({
|
|
7
8
|
children,
|
|
@@ -54,26 +55,16 @@ function SelectTrigger({
|
|
|
54
55
|
data-slot="select-trigger"
|
|
55
56
|
data-size={size}
|
|
56
57
|
className={cn(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// an empty string (value unset and the placeholder not surfaced through
|
|
61
|
-
// the render-prop); `data-[size=sm]` keeps its own floor for the same
|
|
62
|
-
// reason.
|
|
63
|
-
//
|
|
64
|
-
// `sm` is the toolbar size, matching `Button size="sm" variant="outline"`
|
|
65
|
-
// exactly — height, gap, padding, text, icon size and the resting lift.
|
|
66
|
-
// A row mixing a select with a popover trigger (the dashboard's channel
|
|
67
|
-
// picker beside its date range) then reads as one set of controls
|
|
68
|
-
// rather than two. The default size is a form field and stays flat.
|
|
69
|
-
"group/select-trigger flex w-full min-h-8 cursor-pointer items-center justify-between gap-1.5 rounded-md border border-border shadow-control bg-card py-1.5 pr-2 pl-2.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none select-none 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 data-[placeholder]:text-muted-foreground data-[size=sm]:shadow-control data-[size=sm]:h-8 data-[size=sm]:min-h-8 data-[size=sm]:gap-2 data-[size=sm]:px-2 data-[size=sm]:py-1 data-[size=sm]:text-sm data-[size=sm]:[&_svg:not([class*='size-'])]:size-3.5 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
58
|
+
'group/select-trigger',
|
|
59
|
+
fieldTriggerClassName,
|
|
60
|
+
"data-[placeholder]:text-muted-foreground data-[size=sm]:[&_svg:not([class*='size-'])]:size-3.5 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5",
|
|
70
61
|
className,
|
|
71
62
|
)}
|
|
72
63
|
{...props}
|
|
73
64
|
>
|
|
74
65
|
{children}
|
|
75
66
|
<SelectPrimitive.Icon>
|
|
76
|
-
<
|
|
67
|
+
<ChevronsUpDownIcon className="pointer-events-none size-4 text-muted-foreground group-data-[size=sm]/select-trigger:size-3.5" />
|
|
77
68
|
</SelectPrimitive.Icon>
|
|
78
69
|
</SelectPrimitive.Trigger>
|
|
79
70
|
)
|
|
@@ -105,7 +96,7 @@ function SelectContent({
|
|
|
105
96
|
data-slot="select-content"
|
|
106
97
|
data-align-trigger={position === 'item-aligned'}
|
|
107
98
|
className={cn(
|
|
108
|
-
'relative z-[100] max-h-[var(--available-height)] min-w-[var(--anchor-width)] overflow-x-hidden overflow-y-auto rounded-
|
|
99
|
+
'relative z-[100] max-h-[var(--available-height)] min-w-[var(--anchor-width)] overflow-x-hidden overflow-y-auto rounded-xl bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform]',
|
|
109
100
|
position === 'popper' &&
|
|
110
101
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
|
111
102
|
className,
|
package/src/ui/sheet.tsx
CHANGED
|
@@ -129,13 +129,21 @@ function SheetContent({
|
|
|
129
129
|
// `translate`; the rest are listed because `className` is merged, so
|
|
130
130
|
// a caller can animate any of them — and a missing entry fails
|
|
131
131
|
// silently, as a jump rather than an error.
|
|
132
|
-
'fixed z-50 flex flex-col border border-border bg-card text-foreground text-
|
|
133
|
-
'data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-[calc(100dvh-1rem)] data-[side=right]:max-h-[calc(100dvh-1rem)] data-[side=right]:w-[calc(100vw-1rem)] data-[side=right]:max-w-[600px] data-[side=right]:min-w-[320px] data-[side=right]:m-2 data-[side=right]:rounded-
|
|
132
|
+
'fixed z-50 flex flex-col border border-border bg-card text-foreground text-base shadow-lg transition-[opacity,transform,translate,scale] duration-250 ease-out',
|
|
133
|
+
'data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-[calc(100dvh-1rem)] data-[side=right]:max-h-[calc(100dvh-1rem)] data-[side=right]:w-[calc(100vw-1rem)] data-[side=right]:max-w-[600px] data-[side=right]:min-w-[320px] data-[side=right]:m-2 data-[side=right]:rounded-2xl',
|
|
134
134
|
'data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:sm:max-w-sm',
|
|
135
135
|
'data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t',
|
|
136
136
|
'data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b',
|
|
137
137
|
'data-[starting-style]:opacity-0 data-[side=right]:data-[starting-style]:translate-x-10 data-[side=left]:data-[starting-style]:-translate-x-10 data-[side=bottom]:data-[starting-style]:translate-y-10 data-[side=top]:data-[starting-style]:-translate-y-10',
|
|
138
138
|
'data-[ending-style]:opacity-0 data-[side=right]:data-[ending-style]:translate-x-10 data-[side=left]:data-[ending-style]:-translate-x-10 data-[side=bottom]:data-[ending-style]:translate-y-10 data-[side=top]:data-[ending-style]:-translate-y-10',
|
|
139
|
+
// A sheet opened from inside this one stacks on top, as toasts do:
|
|
140
|
+
// this one shrinks from its outer edge and slides out far enough to
|
|
141
|
+
// peek from behind, a step per open sheet above it, and dims.
|
|
142
|
+
'after:pointer-events-none after:absolute after:inset-0 after:z-20 after:rounded-[inherit] after:bg-black/0 after:transition-colors after:duration-250',
|
|
143
|
+
'data-[nested-dialog-open]:after:bg-black/5 dark:data-[nested-dialog-open]:after:bg-black/30',
|
|
144
|
+
'data-[side=right]:origin-left data-[side=left]:origin-right',
|
|
145
|
+
'data-[nested-dialog-open]:scale-[calc(1-0.05*var(--nested-dialogs))]',
|
|
146
|
+
'data-[side=right]:data-[nested-dialog-open]:translate-x-[calc(-20px*var(--nested-dialogs))] data-[side=left]:data-[nested-dialog-open]:translate-x-[calc(20px*var(--nested-dialogs))]',
|
|
139
147
|
className,
|
|
140
148
|
)}
|
|
141
149
|
{...props}
|
|
@@ -207,7 +215,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
207
215
|
<div
|
|
208
216
|
data-slot="sheet-footer"
|
|
209
217
|
className={cn(
|
|
210
|
-
'flex flex-col-reverse gap-2 border-t border-border-subtle bg-muted/50 p-4 sm:flex-row sm:justify-end rounded-b-
|
|
218
|
+
'flex flex-col-reverse gap-2 border-t border-border-subtle bg-muted/50 p-4 sm:flex-row sm:justify-end rounded-b-2xl',
|
|
211
219
|
className,
|
|
212
220
|
)}
|
|
213
221
|
{...props}
|
|
@@ -219,7 +227,7 @@ function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPr
|
|
|
219
227
|
return (
|
|
220
228
|
<SheetPrimitive.Title
|
|
221
229
|
data-slot="sheet-title"
|
|
222
|
-
className={cn('text-lg font-
|
|
230
|
+
className={cn('text-lg font-semibold', className)}
|
|
223
231
|
{...props}
|
|
224
232
|
/>
|
|
225
233
|
)
|
|
@@ -232,7 +240,7 @@ function SheetDescription({
|
|
|
232
240
|
return (
|
|
233
241
|
<SheetPrimitive.Description
|
|
234
242
|
data-slot="sheet-description"
|
|
235
|
-
className={cn('text-
|
|
243
|
+
className={cn('text-base text-muted-foreground', className)}
|
|
236
244
|
{...props}
|
|
237
245
|
/>
|
|
238
246
|
)
|