@spree/dashboard-ui 1.0.0-rc.4 → 1.0.0-rc.5
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 +3 -2
- package/src/index.ts +1 -0
- package/src/spree/address-block.tsx +7 -10
- package/src/spree/address-book-row.tsx +18 -4
- package/src/spree/address-select.tsx +105 -0
- package/src/spree/calculator-summary.tsx +9 -14
- package/src/spree/claim-resolve-dialog.tsx +10 -12
- package/src/spree/form-actions.tsx +27 -5
- package/src/ui/badge.tsx +21 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-ui",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.5",
|
|
5
5
|
"description": "Spree Dashboard design system — shadcn primitives, headless composed components, and design tokens. Source-only package; consumers compile via their own Vite/Tailwind setup. Developer Preview.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"recharts": "3.10.1",
|
|
86
86
|
"shadcn": "4.22.0",
|
|
87
87
|
"tailwind-merge": "3.7.0",
|
|
88
|
-
"tw-animate-css": "1.4.0"
|
|
88
|
+
"tw-animate-css": "1.4.0",
|
|
89
|
+
"@spree/core": "^0.2.0"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
92
|
"@types/react": "^19.2.14",
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { decimalPlacesMessage, requiredMessage } from './lib/validation-messages
|
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
export * from './spree/address-block'
|
|
34
34
|
export * from './spree/address-book-row'
|
|
35
|
+
export * from './spree/address-select'
|
|
35
36
|
export * from './spree/back-button'
|
|
36
37
|
export * from './spree/bulk-dialog'
|
|
37
38
|
export * from './spree/bulk-price-table'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { intlDisplayName } from '@spree/core'
|
|
1
2
|
import { useTranslation } from 'react-i18next'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -28,24 +29,20 @@ function localizedCountryName(
|
|
|
28
29
|
iso?: string | null,
|
|
29
30
|
name?: string | null,
|
|
30
31
|
): string | null {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const localized = new Intl.DisplayNames([locale, 'en'], { type: 'region' }).of(iso)
|
|
34
|
-
if (localized) return localized
|
|
35
|
-
} catch {
|
|
36
|
-
// Runtime without Intl.DisplayNames coverage — fall back below.
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return name ?? iso ?? null
|
|
32
|
+
const localized = iso ? intlDisplayName('region', locale, iso) : undefined
|
|
33
|
+
return localized || (name ?? iso ?? null)
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
export function AddressBlock({
|
|
43
37
|
title,
|
|
44
38
|
address,
|
|
39
|
+
emptyText,
|
|
45
40
|
}: {
|
|
46
41
|
/** Omitted where the surrounding card already names the address. */
|
|
47
42
|
title?: string
|
|
48
43
|
address: AddressBlockValue | null | undefined
|
|
44
|
+
/** Shown in place of the address when there is none. Defaults to "Not provided". */
|
|
45
|
+
emptyText?: string
|
|
49
46
|
}) {
|
|
50
47
|
const { t, i18n } = useTranslation()
|
|
51
48
|
return (
|
|
@@ -67,7 +64,7 @@ export function AddressBlock({
|
|
|
67
64
|
</div>
|
|
68
65
|
) : (
|
|
69
66
|
<span className="text-sm text-muted-foreground">
|
|
70
|
-
{t('admin.components.address_block.not_provided')}
|
|
67
|
+
{emptyText ?? t('admin.components.address_block.not_provided')}
|
|
71
68
|
</span>
|
|
72
69
|
)}
|
|
73
70
|
</div>
|
|
@@ -23,6 +23,7 @@ export interface AddressBookEntry extends AddressBlockValue {
|
|
|
23
23
|
export function AddressBookRow({
|
|
24
24
|
address,
|
|
25
25
|
canEdit = true,
|
|
26
|
+
inheritedFrom,
|
|
26
27
|
onEdit,
|
|
27
28
|
onRemove,
|
|
28
29
|
onSetDefaultBilling,
|
|
@@ -30,6 +31,11 @@ export function AddressBookRow({
|
|
|
30
31
|
}: {
|
|
31
32
|
address: AddressBookEntry
|
|
32
33
|
canEdit?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Names the book this entry is inherited from. An inherited entry is read
|
|
36
|
+
* only here, and its defaults are its owner's, so neither is shown.
|
|
37
|
+
*/
|
|
38
|
+
inheritedFrom?: string | null
|
|
33
39
|
onEdit: () => void
|
|
34
40
|
onRemove: () => void
|
|
35
41
|
/** Omit either to hide that action — a book with one address has no choice. */
|
|
@@ -37,17 +43,25 @@ export function AddressBookRow({
|
|
|
37
43
|
onSetDefaultShipping?: () => void
|
|
38
44
|
}) {
|
|
39
45
|
const { t } = useTranslation()
|
|
46
|
+
const inherited = inheritedFrom != null
|
|
47
|
+
const isDefaultBilling = !inherited && address.is_default_billing
|
|
48
|
+
const isDefaultShipping = !inherited && address.is_default_shipping
|
|
40
49
|
|
|
41
50
|
return (
|
|
42
51
|
<Item variant="outline" className="items-start">
|
|
43
52
|
<ItemContent className="min-w-0">
|
|
44
|
-
{(address.label ||
|
|
53
|
+
{(address.label || inherited || isDefaultBilling || isDefaultShipping) && (
|
|
45
54
|
<ItemTitle className="flex-wrap">
|
|
46
55
|
{address.label}
|
|
47
|
-
{
|
|
56
|
+
{inherited && (
|
|
57
|
+
<Badge variant="secondary">
|
|
58
|
+
{t('admin.address_book.inherited_from', { name: inheritedFrom })}
|
|
59
|
+
</Badge>
|
|
60
|
+
)}
|
|
61
|
+
{isDefaultBilling && (
|
|
48
62
|
<Badge variant="outline">{t('admin.address_book.default_billing')}</Badge>
|
|
49
63
|
)}
|
|
50
|
-
{
|
|
64
|
+
{isDefaultShipping && (
|
|
51
65
|
<Badge variant="outline">{t('admin.address_book.default_shipping')}</Badge>
|
|
52
66
|
)}
|
|
53
67
|
</ItemTitle>
|
|
@@ -55,7 +69,7 @@ export function AddressBookRow({
|
|
|
55
69
|
<AddressBlock address={address} />
|
|
56
70
|
</ItemContent>
|
|
57
71
|
|
|
58
|
-
{canEdit && (
|
|
72
|
+
{canEdit && !inherited && (
|
|
59
73
|
<ItemActions>
|
|
60
74
|
<RowActions
|
|
61
75
|
actions={[
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
2
|
+
import {
|
|
3
|
+
Select,
|
|
4
|
+
SelectContent,
|
|
5
|
+
SelectGroup,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectLabel,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '../ui/select'
|
|
11
|
+
import type { AddressBlockValue } from './address-block'
|
|
12
|
+
|
|
13
|
+
/** One entry a picker may offer, from whichever book it came out of. */
|
|
14
|
+
export interface AddressOption extends AddressBlockValue {
|
|
15
|
+
id: string
|
|
16
|
+
label?: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A set of entries under the name of the book they came from — a customer's
|
|
21
|
+
* own addresses, a company's sites. Groups are rendered in the order given.
|
|
22
|
+
*/
|
|
23
|
+
export interface AddressOptionGroup {
|
|
24
|
+
key: string
|
|
25
|
+
heading: string
|
|
26
|
+
addresses: AddressOption[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The one-line form a picker shows: the entry's own name, then the street. */
|
|
30
|
+
export function summarizeAddress(address: AddressOption): string {
|
|
31
|
+
const place = [address.address1, address.city, address.state_text, address.postal_code]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(', ')
|
|
34
|
+
|
|
35
|
+
const who = address.label || address.company || address.full_name
|
|
36
|
+
|
|
37
|
+
return [who, place].filter(Boolean).join(' — ')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Picks one saved address out of the books a purchase may draw on.
|
|
42
|
+
*
|
|
43
|
+
* Headless: the groups arrive as props, so the same control serves a buyer's
|
|
44
|
+
* own book, a company's sites, and both at once — which is the B2B case, where
|
|
45
|
+
* an order for a company may ship to that company's sites and to the ones it
|
|
46
|
+
* inherits from the companies above it.
|
|
47
|
+
*
|
|
48
|
+
* Base UI resolves the trigger's label from `items`, so the summary line shows
|
|
49
|
+
* instead of the raw prefixed id.
|
|
50
|
+
*/
|
|
51
|
+
export function AddressSelect({
|
|
52
|
+
id,
|
|
53
|
+
value,
|
|
54
|
+
onValueChange,
|
|
55
|
+
groups,
|
|
56
|
+
placeholder,
|
|
57
|
+
emptyLabel,
|
|
58
|
+
disabled,
|
|
59
|
+
}: {
|
|
60
|
+
id?: string
|
|
61
|
+
/** The chosen address id, or `''` for "none picked". */
|
|
62
|
+
value: string
|
|
63
|
+
onValueChange: (value: string) => void
|
|
64
|
+
groups: AddressOptionGroup[]
|
|
65
|
+
placeholder?: string
|
|
66
|
+
/** Offered first so a merchant can take the choice back. */
|
|
67
|
+
emptyLabel?: string
|
|
68
|
+
disabled?: boolean
|
|
69
|
+
}) {
|
|
70
|
+
const { t } = useTranslation()
|
|
71
|
+
const none = emptyLabel ?? t('admin.address_select.none')
|
|
72
|
+
|
|
73
|
+
const items = [
|
|
74
|
+
{ value: '', label: none },
|
|
75
|
+
...groups.flatMap((group) =>
|
|
76
|
+
group.addresses.map((address) => ({
|
|
77
|
+
value: address.id,
|
|
78
|
+
label: summarizeAddress(address),
|
|
79
|
+
})),
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Select items={items} value={value} onValueChange={onValueChange} disabled={disabled}>
|
|
85
|
+
<SelectTrigger id={id}>
|
|
86
|
+
<SelectValue placeholder={placeholder ?? none} />
|
|
87
|
+
</SelectTrigger>
|
|
88
|
+
<SelectContent>
|
|
89
|
+
<SelectItem value="">{none}</SelectItem>
|
|
90
|
+
{groups
|
|
91
|
+
.filter((group) => group.addresses.length > 0)
|
|
92
|
+
.map((group) => (
|
|
93
|
+
<SelectGroup key={group.key}>
|
|
94
|
+
<SelectLabel>{group.heading}</SelectLabel>
|
|
95
|
+
{group.addresses.map((address) => (
|
|
96
|
+
<SelectItem key={address.id} value={address.id}>
|
|
97
|
+
{summarizeAddress(address)}
|
|
98
|
+
</SelectItem>
|
|
99
|
+
))}
|
|
100
|
+
</SelectGroup>
|
|
101
|
+
))}
|
|
102
|
+
</SelectContent>
|
|
103
|
+
</Select>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* Lives on the frontend (rather than as a server-rendered string) so
|
|
14
14
|
* draft edits update the row immediately, before any save.
|
|
15
15
|
*/
|
|
16
|
+
|
|
17
|
+
import { formatMoney, isDecimalString } from '@spree/core'
|
|
18
|
+
|
|
16
19
|
export interface CalculatorPayload {
|
|
17
20
|
/**
|
|
18
21
|
* Display name for the calculator, already localized by the caller — this
|
|
@@ -101,7 +104,12 @@ function formatField(
|
|
|
101
104
|
): string | null {
|
|
102
105
|
if (value === null || value === undefined || value === '') return null
|
|
103
106
|
if (property.format === 'currency') return null
|
|
104
|
-
if (property.format === 'money')
|
|
107
|
+
if (property.format === 'money') {
|
|
108
|
+
const amount = String(value)
|
|
109
|
+
return (
|
|
110
|
+
formatMoney(amount, currency) || (isDecimalString(amount) ? `${amount} ${currency}` : amount)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
105
113
|
if (/(?:^|_)percent$/.test(key)) return `${value}%`
|
|
106
114
|
|
|
107
115
|
const type = (Array.isArray(property.type) ? property.type : [property.type]).find(
|
|
@@ -115,19 +123,6 @@ function formatField(
|
|
|
115
123
|
return `${humanize(key)}: ${value}`
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
function formatMoney(value: unknown, currency: string): string {
|
|
119
|
-
const amount = String(value)
|
|
120
|
-
if (!/^-?\d+(\.\d+)?$/.test(amount)) return amount
|
|
121
|
-
try {
|
|
122
|
-
// `Intl` reads a numeric string exactly; its ES2022 typings only list numbers.
|
|
123
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(
|
|
124
|
-
amount as unknown as number,
|
|
125
|
-
)
|
|
126
|
-
} catch {
|
|
127
|
-
return `${amount} ${currency}`
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
126
|
function humanize(key: string): string {
|
|
132
127
|
const spaced = key.replace(/_/g, ' ').trim()
|
|
133
128
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLAIM_RESOLUTIONS, type ClaimResolution, isPositiveMoney } from '@spree/core'
|
|
1
2
|
import { useState } from 'react'
|
|
2
3
|
import { useTranslation } from 'react-i18next'
|
|
3
4
|
import { Button } from '../ui/button'
|
|
@@ -16,8 +17,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
|
|
16
17
|
import { Switch } from '../ui/switch'
|
|
17
18
|
import type { RefundMethod } from './return-dialogs'
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
export type ClaimResolution = 'refund' | 'replacement' | 'refund_and_replacement'
|
|
20
|
+
export type { ClaimResolution }
|
|
21
21
|
|
|
22
22
|
/** One claimed line, as the resolve form needs it. */
|
|
23
23
|
export type ClaimLineForResolve = {
|
|
@@ -74,12 +74,10 @@ export function ClaimResolveDialog({
|
|
|
74
74
|
.filter(([, on]) => on)
|
|
75
75
|
.map(([id]) => id)
|
|
76
76
|
|
|
77
|
-
const resolutionOptions = (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
82
|
-
)
|
|
77
|
+
const resolutionOptions = CLAIM_RESOLUTIONS.map((value) => ({
|
|
78
|
+
value,
|
|
79
|
+
label: t(`admin.pages.orders.detail.claims.resolutions.${value}`),
|
|
80
|
+
}))
|
|
83
81
|
|
|
84
82
|
const methodOptions = [
|
|
85
83
|
{
|
|
@@ -92,11 +90,11 @@ export function ClaimResolveDialog({
|
|
|
92
90
|
},
|
|
93
91
|
]
|
|
94
92
|
|
|
93
|
+
// A number input keeps `.5` as typed; the API reads only `0.5`.
|
|
94
|
+
const refundAmount = amount.startsWith('.') ? `0${amount}` : amount
|
|
95
95
|
// Refunding nothing, or replacing nothing, is what the server rejects —
|
|
96
96
|
// say so here instead of letting the request fail.
|
|
97
|
-
|
|
98
|
-
// "above zero" is unsigned digits with a non-zero digit among them.
|
|
99
|
-
const positiveAmount = /^(\d+(\.\d+)?|\.\d+)$/.test(amount) && /[1-9]/.test(amount)
|
|
97
|
+
const positiveAmount = isPositiveMoney(refundAmount)
|
|
100
98
|
const ready =
|
|
101
99
|
(!refunding || positiveAmount) && (!sendingReplacement || chosenReplacements.length > 0)
|
|
102
100
|
|
|
@@ -211,7 +209,7 @@ export function ClaimResolveDialog({
|
|
|
211
209
|
onSubmit({
|
|
212
210
|
resolution,
|
|
213
211
|
refundMethod,
|
|
214
|
-
amount: refunding ?
|
|
212
|
+
amount: refunding ? refundAmount : undefined,
|
|
215
213
|
replacementLineItemIds: chosenReplacements,
|
|
216
214
|
})
|
|
217
215
|
}
|
|
@@ -22,16 +22,22 @@ export function FormSaveButton<TFieldValues extends FieldValues>({
|
|
|
22
22
|
form,
|
|
23
23
|
label,
|
|
24
24
|
savingLabel,
|
|
25
|
+
dirty = false,
|
|
25
26
|
}: {
|
|
26
27
|
form: UseFormReturn<TFieldValues, any, any>
|
|
27
28
|
label?: string
|
|
28
29
|
savingLabel?: string
|
|
30
|
+
/**
|
|
31
|
+
* Unsaved changes held outside the form — custom field drafts the page
|
|
32
|
+
* saves from its own submit — counted as dirty alongside the form's own.
|
|
33
|
+
*/
|
|
34
|
+
dirty?: boolean
|
|
29
35
|
}) {
|
|
30
36
|
const { t } = useTranslation()
|
|
31
37
|
const { isDirty, isSubmitting } = useFormState({ control: form.control })
|
|
32
38
|
|
|
33
39
|
return (
|
|
34
|
-
<Button type="submit" disabled={!isDirty || isSubmitting}>
|
|
40
|
+
<Button type="submit" disabled={!(isDirty || dirty) || isSubmitting}>
|
|
35
41
|
{isSubmitting && <Loader2Icon className="animate-spin" />}
|
|
36
42
|
{isSubmitting
|
|
37
43
|
? (savingLabel ?? t('admin.actions.saving'))
|
|
@@ -52,15 +58,22 @@ export function FormDiscardButton<TFieldValues extends FieldValues>({
|
|
|
52
58
|
form,
|
|
53
59
|
label,
|
|
54
60
|
onDiscard,
|
|
61
|
+
dirty = false,
|
|
55
62
|
}: {
|
|
56
63
|
form: UseFormReturn<TFieldValues, any, any>
|
|
57
64
|
label?: string
|
|
58
65
|
onDiscard?: () => void
|
|
66
|
+
/**
|
|
67
|
+
* Unsaved changes held outside the form — custom field drafts the page
|
|
68
|
+
* saves from its own submit — counted as dirty alongside the form's own.
|
|
69
|
+
* Pass `onDiscard` too, so discarding drops them.
|
|
70
|
+
*/
|
|
71
|
+
dirty?: boolean
|
|
59
72
|
}) {
|
|
60
73
|
const { t } = useTranslation()
|
|
61
74
|
const { isDirty, isSubmitting } = useFormState({ control: form.control })
|
|
62
75
|
|
|
63
|
-
if (!isDirty) return null
|
|
76
|
+
if (!isDirty && !dirty) return null
|
|
64
77
|
|
|
65
78
|
return (
|
|
66
79
|
<Button
|
|
@@ -83,17 +96,23 @@ export function FormActions<TFieldValues extends FieldValues>({
|
|
|
83
96
|
savingLabel,
|
|
84
97
|
discardLabel,
|
|
85
98
|
onDiscard,
|
|
99
|
+
dirty,
|
|
86
100
|
}: {
|
|
87
101
|
form: UseFormReturn<TFieldValues, any, any>
|
|
88
102
|
saveLabel?: string
|
|
89
103
|
savingLabel?: string
|
|
90
104
|
discardLabel?: string
|
|
91
105
|
onDiscard?: () => void
|
|
106
|
+
/**
|
|
107
|
+
* Unsaved changes held outside the form — custom field drafts the page
|
|
108
|
+
* saves from its own submit — counted as dirty alongside the form's own.
|
|
109
|
+
*/
|
|
110
|
+
dirty?: boolean
|
|
92
111
|
}) {
|
|
93
112
|
return (
|
|
94
113
|
<>
|
|
95
|
-
<FormDiscardButton form={form} label={discardLabel} onDiscard={onDiscard} />
|
|
96
|
-
<FormSaveButton form={form} label={saveLabel} savingLabel={savingLabel} />
|
|
114
|
+
<FormDiscardButton form={form} label={discardLabel} onDiscard={onDiscard} dirty={dirty} />
|
|
115
|
+
<FormSaveButton form={form} label={saveLabel} savingLabel={savingLabel} dirty={dirty} />
|
|
97
116
|
</>
|
|
98
117
|
)
|
|
99
118
|
}
|
|
@@ -113,8 +132,11 @@ export function FormActions<TFieldValues extends FieldValues>({
|
|
|
113
132
|
export function useFormSubmitShortcut<TFieldValues extends FieldValues>(
|
|
114
133
|
form: UseFormReturn<TFieldValues, any, any>,
|
|
115
134
|
onSubmit: (values: TFieldValues) => void | Promise<void>,
|
|
135
|
+
/** Unsaved changes held outside the form, as on `FormActions`. */
|
|
136
|
+
{ dirty: externalDirty = false }: { dirty?: boolean } = {},
|
|
116
137
|
) {
|
|
117
|
-
const { isDirty, isSubmitting } = useFormState({ control: form.control })
|
|
138
|
+
const { isDirty: formDirty, isSubmitting } = useFormState({ control: form.control })
|
|
139
|
+
const isDirty = formDirty || externalDirty
|
|
118
140
|
|
|
119
141
|
useHotkey(
|
|
120
142
|
'Mod+S',
|
package/src/ui/badge.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BuiltInStatus } from '@spree/core'
|
|
1
2
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
3
|
import type * as React from 'react'
|
|
3
4
|
import { useTranslation } from 'react-i18next'
|
|
@@ -80,6 +81,7 @@ const statusToneMap: Record<string, StatusTone> = {
|
|
|
80
81
|
authorized: 'info',
|
|
81
82
|
complete: 'success',
|
|
82
83
|
completed: 'success',
|
|
84
|
+
placed: 'success',
|
|
83
85
|
paid: 'success',
|
|
84
86
|
fulfilled: 'success',
|
|
85
87
|
delivered: 'success',
|
|
@@ -157,18 +159,36 @@ const statusToneMap: Record<string, StatusTone> = {
|
|
|
157
159
|
ready_to_ship: 'warning',
|
|
158
160
|
ordered: 'info',
|
|
159
161
|
partially_received: 'info',
|
|
162
|
+
// More arrived than was ordered: done, but worth a look.
|
|
163
|
+
over_received: 'warning',
|
|
160
164
|
// Gift cards.
|
|
161
165
|
partially_redeemed: 'info',
|
|
162
166
|
redeemed: 'neutral',
|
|
163
167
|
// Price lists — `scheduled` is live-but-not-yet, which is the same shape as
|
|
164
168
|
// any other "waiting on the clock" state.
|
|
165
169
|
scheduled: 'info',
|
|
170
|
+
// The rest of the API's own statuses, on the same rule as above.
|
|
171
|
+
accepted: 'success',
|
|
172
|
+
waived: 'neutral',
|
|
173
|
+
withdrawn: 'neutral',
|
|
174
|
+
awaiting_confirmation: 'warning',
|
|
175
|
+
lapsed: 'neutral',
|
|
176
|
+
mapping: 'warning',
|
|
177
|
+
completed_mapping: 'info',
|
|
178
|
+
unresolved: 'destructive',
|
|
179
|
+
on_hand: 'success',
|
|
180
|
+
backordered: 'warning',
|
|
181
|
+
shipped: 'success',
|
|
182
|
+
checkout: 'neutral',
|
|
183
|
+
invalid: 'destructive',
|
|
184
|
+
purchased: 'success',
|
|
185
|
+
refund_requested: 'warning',
|
|
166
186
|
// Boolean filter values. A yes/no pair is a state like any other — "in
|
|
167
187
|
// stock: no" is the row an operator is looking for, and reading it in the
|
|
168
188
|
// same green/red the rest of the app uses saves them parsing the word.
|
|
169
189
|
true: 'success',
|
|
170
190
|
false: 'destructive',
|
|
171
|
-
}
|
|
191
|
+
} satisfies Record<BuiltInStatus, StatusTone> & Record<string, StatusTone>
|
|
172
192
|
|
|
173
193
|
// Dot fills, by tone. These read the same `--status-*` ramps the tinted Badge
|
|
174
194
|
// variants use, but take the *reading* step rather than the fill step: a 6px
|