@spree/dashboard-ui 6.0.2 → 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
|
@@ -3,12 +3,41 @@ import { Field, FieldLabel } from '../ui/field'
|
|
|
3
3
|
import { Input } from '../ui/input'
|
|
4
4
|
import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
|
|
5
5
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
|
6
|
+
import { ProductIdentity } from './product-identity'
|
|
6
7
|
|
|
7
8
|
/** A unit on the order that a return, exchange or claim can name. */
|
|
8
9
|
export type PostSaleUnit = {
|
|
9
10
|
id: string
|
|
11
|
+
/** Also the quantity field's accessible name. */
|
|
10
12
|
label: string
|
|
11
13
|
quantity: number
|
|
14
|
+
/** Shown as the product it is — image, name, options — when known. */
|
|
15
|
+
product?: PostSaleUnitProduct
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PostSaleUnitProduct = {
|
|
19
|
+
name: string
|
|
20
|
+
optionsText?: string | null
|
|
21
|
+
thumbnailUrl?: string | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A unit as the product it is, the same row the order's item lists use, or
|
|
26
|
+
* its plain label when the caller has nothing more.
|
|
27
|
+
*/
|
|
28
|
+
export function PostSaleUnitLabel({ unit }: { unit: PostSaleUnit }) {
|
|
29
|
+
if (!unit.product) {
|
|
30
|
+
return <ItemTitle className="block truncate font-normal">{unit.label}</ItemTitle>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ProductIdentity
|
|
35
|
+
name={unit.product.name}
|
|
36
|
+
subtitle={unit.product.optionsText}
|
|
37
|
+
thumbnailUrl={unit.product.thumbnailUrl}
|
|
38
|
+
size="sm"
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
/** How many of each unit a record covers; zero means "not included". */
|
|
@@ -34,7 +63,7 @@ export function QuantityPicker({
|
|
|
34
63
|
{units.map((unit) => (
|
|
35
64
|
<Item key={unit.id} variant="outline" className="flex-nowrap border-border-subtle">
|
|
36
65
|
<ItemContent className="min-w-0">
|
|
37
|
-
<
|
|
66
|
+
<PostSaleUnitLabel unit={unit} />
|
|
38
67
|
</ItemContent>
|
|
39
68
|
<ItemActions>
|
|
40
69
|
<Input
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mergeProps } from '@base-ui/react/merge-props'
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
import { Thumbnail } from '../ui/thumbnail'
|
|
6
|
+
import { PackageIcon } from './icons'
|
|
7
|
+
|
|
8
|
+
export interface ProductIdentityProps extends useRender.ComponentProps<'span'> {
|
|
9
|
+
name?: string | null
|
|
10
|
+
/** A second line: the SKU, option values or a variant's name. */
|
|
11
|
+
subtitle?: ReactNode
|
|
12
|
+
thumbnailUrl?: string | null
|
|
13
|
+
size?: 'sm' | 'md' | 'lg'
|
|
14
|
+
/** Badges and other markers shown after the name. */
|
|
15
|
+
badges?: ReactNode
|
|
16
|
+
/** Strikes the name through, for a row that is about to be removed. */
|
|
17
|
+
struckThrough?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A product or variant, the same way in every list: its image, its name and
|
|
22
|
+
* an optional second line.
|
|
23
|
+
*
|
|
24
|
+
* Pass `render` (a router link, or a button a table listens to) to make the
|
|
25
|
+
* whole block the target — image and name together, never the name alone.
|
|
26
|
+
*/
|
|
27
|
+
export function ProductIdentity({
|
|
28
|
+
name,
|
|
29
|
+
subtitle,
|
|
30
|
+
thumbnailUrl,
|
|
31
|
+
size = 'md',
|
|
32
|
+
badges,
|
|
33
|
+
struckThrough,
|
|
34
|
+
render,
|
|
35
|
+
className,
|
|
36
|
+
...props
|
|
37
|
+
}: ProductIdentityProps) {
|
|
38
|
+
const interactive = render !== undefined
|
|
39
|
+
|
|
40
|
+
return useRender({
|
|
41
|
+
defaultTagName: 'span',
|
|
42
|
+
render,
|
|
43
|
+
props: mergeProps<'span'>(
|
|
44
|
+
{
|
|
45
|
+
className: cn(
|
|
46
|
+
'flex min-w-0 items-center gap-3 text-left text-foreground',
|
|
47
|
+
interactive &&
|
|
48
|
+
'group cursor-pointer rounded-md no-underline outline-none focus-visible:shadow-field-focus',
|
|
49
|
+
className,
|
|
50
|
+
),
|
|
51
|
+
children: (
|
|
52
|
+
<>
|
|
53
|
+
<Thumbnail src={thumbnailUrl} size={size} fallback={<PackageIcon />} />
|
|
54
|
+
<span className="flex min-w-0 flex-col">
|
|
55
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
56
|
+
<span
|
|
57
|
+
className={cn('truncate font-medium text-base', struckThrough && 'line-through')}
|
|
58
|
+
>
|
|
59
|
+
{name ?? '—'}
|
|
60
|
+
</span>
|
|
61
|
+
{badges}
|
|
62
|
+
</span>
|
|
63
|
+
{subtitle && (
|
|
64
|
+
<span className="truncate text-muted-foreground text-xs">{subtitle}</span>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
</>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
props,
|
|
71
|
+
),
|
|
72
|
+
state: { slot: 'product-identity' },
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -15,16 +15,17 @@ import {
|
|
|
15
15
|
verticalListSortingStrategy,
|
|
16
16
|
} from '@dnd-kit/sortable'
|
|
17
17
|
import { CSS } from '@dnd-kit/utilities'
|
|
18
|
-
import { Fragment, type ReactNode, useEffect, useMemo, useState } from 'react'
|
|
18
|
+
import { Fragment, type ReactElement, type ReactNode, useEffect, useMemo, useState } from 'react'
|
|
19
19
|
import { cn } from '../lib/utils'
|
|
20
20
|
import { Badge } from '../ui/badge'
|
|
21
21
|
import { Button } from '../ui/button'
|
|
22
22
|
import { Checkbox } from '../ui/checkbox'
|
|
23
23
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/data-table'
|
|
24
|
+
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty'
|
|
24
25
|
import { Pagination, type PaginationMeta } from '../ui/pagination'
|
|
25
|
-
import { Thumbnail } from '../ui/thumbnail'
|
|
26
26
|
import { DragHandle } from './drag-handle'
|
|
27
|
-
import { Undo2Icon, XIcon } from './icons'
|
|
27
|
+
import { PackageIcon, Undo2Icon, XIcon } from './icons'
|
|
28
|
+
import { ProductIdentity } from './product-identity'
|
|
28
29
|
import { SearchInput } from './search-input'
|
|
29
30
|
|
|
30
31
|
/** One product row in a membership list. */
|
|
@@ -92,8 +93,8 @@ export interface ProductMembershipListProps {
|
|
|
92
93
|
* (page offset already applied). A rejected promise rolls the row back.
|
|
93
94
|
*/
|
|
94
95
|
onReorder?: (id: string, position: number) => Promise<unknown> | undefined
|
|
95
|
-
/**
|
|
96
|
-
|
|
96
|
+
/** The element a row's image and name render as — a link to the product page. */
|
|
97
|
+
rowLink?: (row: ProductMembershipRow) => ReactElement
|
|
97
98
|
/**
|
|
98
99
|
* Extra columns between the product name and the remove button, for a
|
|
99
100
|
* parent whose membership carries data of its own — a catalog's quantity
|
|
@@ -143,7 +144,7 @@ export function ProductMembershipList({
|
|
|
143
144
|
onRestore,
|
|
144
145
|
reorderable = false,
|
|
145
146
|
onReorder,
|
|
146
|
-
|
|
147
|
+
rowLink,
|
|
147
148
|
renderSubRows,
|
|
148
149
|
extraColumns,
|
|
149
150
|
}: ProductMembershipListProps) {
|
|
@@ -229,17 +230,38 @@ export function ProductMembershipList({
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
if (loading) {
|
|
232
|
-
return <p className="p-6 text-muted-foreground text-
|
|
233
|
+
return <p className="p-6 text-muted-foreground text-base">{labels.loading}</p>
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
if (order.length === 0) {
|
|
236
|
-
return
|
|
237
|
+
return (
|
|
238
|
+
<Empty>
|
|
239
|
+
<EmptyHeader>
|
|
240
|
+
<EmptyMedia variant="icon">
|
|
241
|
+
<PackageIcon />
|
|
242
|
+
</EmptyMedia>
|
|
243
|
+
<EmptyTitle>{labels.empty}</EmptyTitle>
|
|
244
|
+
</EmptyHeader>
|
|
245
|
+
</Empty>
|
|
246
|
+
)
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
// A single product column needs no heading; extra columns (prices) do.
|
|
250
|
+
const showHeader = !!extraColumns
|
|
251
|
+
|
|
239
252
|
return (
|
|
240
253
|
<>
|
|
241
|
-
<div className="border-border-subtle border-b
|
|
254
|
+
<div className="flex items-center gap-3 border-border-subtle border-b px-4 py-3">
|
|
255
|
+
{/* Select-all lives here when the table has no header row to hold it. */}
|
|
256
|
+
{curatable && !showHeader && visible.length > 0 && (
|
|
257
|
+
<Checkbox
|
|
258
|
+
checked={allSelected}
|
|
259
|
+
onCheckedChange={toggleAll}
|
|
260
|
+
aria-label={labels.selectAll}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
242
263
|
<SearchInput
|
|
264
|
+
className="flex-1"
|
|
243
265
|
value={query}
|
|
244
266
|
onValueChange={setQuery}
|
|
245
267
|
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
|
@@ -248,32 +270,43 @@ export function ProductMembershipList({
|
|
|
248
270
|
/>
|
|
249
271
|
</div>
|
|
250
272
|
{visible.length === 0 ? (
|
|
251
|
-
<p className="p-6 text-muted-foreground text-
|
|
273
|
+
<p className="p-6 text-muted-foreground text-base">{labels.noMatches}</p>
|
|
252
274
|
) : (
|
|
253
275
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
254
276
|
<Table>
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
277
|
+
{/* Without a header row, the column widths it set live here. */}
|
|
278
|
+
{!showHeader && (
|
|
279
|
+
<colgroup>
|
|
280
|
+
{curatable && <col className="w-10" />}
|
|
281
|
+
{reorderable && <col className="w-8" />}
|
|
282
|
+
<col className="w-full" />
|
|
283
|
+
<col className="w-10" />
|
|
284
|
+
</colgroup>
|
|
285
|
+
)}
|
|
286
|
+
{showHeader && (
|
|
287
|
+
<TableHeader>
|
|
288
|
+
<TableRow>
|
|
289
|
+
{curatable && (
|
|
290
|
+
<TableHead className="w-10">
|
|
291
|
+
<Checkbox
|
|
292
|
+
checked={allSelected}
|
|
293
|
+
onCheckedChange={toggleAll}
|
|
294
|
+
aria-label={labels.selectAll}
|
|
295
|
+
/>
|
|
296
|
+
</TableHead>
|
|
297
|
+
)}
|
|
298
|
+
{reorderable && <TableHead className="w-8" />}
|
|
299
|
+
{/* `w-full max-w-0`: table layout sizes a column to its
|
|
268
300
|
content, so a long product or variant name would push the
|
|
269
301
|
columns beside it out of the card — which clips them
|
|
270
302
|
rather than scrolling. This makes the product column take
|
|
271
303
|
whatever is left instead, and its content truncate. */}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
<TableHead className="w-full max-w-0">{labels.columnProduct}</TableHead>
|
|
305
|
+
{extraColumns?.headers}
|
|
306
|
+
<TableHead className="w-10" />
|
|
307
|
+
</TableRow>
|
|
308
|
+
</TableHeader>
|
|
309
|
+
)}
|
|
277
310
|
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
|
278
311
|
{/* No border-t: TableHead draws the rule as an inset shadow,
|
|
279
312
|
and a border alongside it stacks into a 2px line. */}
|
|
@@ -292,7 +325,7 @@ export function ProductMembershipList({
|
|
|
292
325
|
onRemove={onRemove ? () => onRemove(row.id) : undefined}
|
|
293
326
|
onRestore={onRestore ? () => onRestore(row.id) : undefined}
|
|
294
327
|
labels={labels}
|
|
295
|
-
|
|
328
|
+
rowLink={rowLink}
|
|
296
329
|
extraCells={extraColumns?.renderCells(row)}
|
|
297
330
|
subRows={renderSubRows?.(row, subRowLayout)}
|
|
298
331
|
/>
|
|
@@ -302,8 +335,7 @@ export function ProductMembershipList({
|
|
|
302
335
|
</Table>
|
|
303
336
|
</DndContext>
|
|
304
337
|
)}
|
|
305
|
-
{
|
|
306
|
-
{meta && onPageChange && <Pagination meta={meta} onPageChange={onPageChange} />}
|
|
338
|
+
{meta && onPageChange && <Pagination inset meta={meta} onPageChange={onPageChange} />}
|
|
307
339
|
</>
|
|
308
340
|
)
|
|
309
341
|
}
|
|
@@ -318,7 +350,7 @@ function MembershipRow({
|
|
|
318
350
|
onRemove,
|
|
319
351
|
onRestore,
|
|
320
352
|
labels,
|
|
321
|
-
|
|
353
|
+
rowLink,
|
|
322
354
|
extraCells,
|
|
323
355
|
subRows,
|
|
324
356
|
}: {
|
|
@@ -331,7 +363,7 @@ function MembershipRow({
|
|
|
331
363
|
onRemove?: () => void
|
|
332
364
|
onRestore?: () => void
|
|
333
365
|
labels: ProductMembershipListLabels
|
|
334
|
-
|
|
366
|
+
rowLink?: ProductMembershipListProps['rowLink']
|
|
335
367
|
extraCells?: ReactNode
|
|
336
368
|
subRows?: ReactNode
|
|
337
369
|
}) {
|
|
@@ -374,13 +406,19 @@ function MembershipRow({
|
|
|
374
406
|
</TableCell>
|
|
375
407
|
)}
|
|
376
408
|
<TableCell className="max-w-0">
|
|
377
|
-
<
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)}
|
|
383
|
-
|
|
409
|
+
<ProductIdentity
|
|
410
|
+
name={row.name}
|
|
411
|
+
thumbnailUrl={row.thumbnailUrl}
|
|
412
|
+
size="sm"
|
|
413
|
+
struckThrough={removed}
|
|
414
|
+
render={rowLink?.(row)}
|
|
415
|
+
badges={
|
|
416
|
+
added &&
|
|
417
|
+
labels.pendingAddedBadge && (
|
|
418
|
+
<Badge variant="secondary">{labels.pendingAddedBadge}</Badge>
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
/>
|
|
384
422
|
</TableCell>
|
|
385
423
|
{extraCells}
|
|
386
424
|
<TableCell className="text-right">
|
|
@@ -400,7 +438,7 @@ function MembershipRow({
|
|
|
400
438
|
: onRemove && (
|
|
401
439
|
<Button
|
|
402
440
|
type="button"
|
|
403
|
-
variant="ghost"
|
|
441
|
+
variant="destructive-ghost"
|
|
404
442
|
size="icon"
|
|
405
443
|
onClick={onRemove}
|
|
406
444
|
aria-label={labels.remove}
|
|
@@ -75,7 +75,7 @@ export function StatusCard<TFieldValues extends FieldValues>({
|
|
|
75
75
|
</Select>
|
|
76
76
|
)}
|
|
77
77
|
/>
|
|
78
|
-
{description && <p className="text-muted-foreground text-
|
|
78
|
+
{description && <p className="text-muted-foreground text-base">{description}</p>}
|
|
79
79
|
</Field>
|
|
80
80
|
{actions && <div className="flex flex-wrap gap-2">{actions}</div>}
|
|
81
81
|
</CardContent>
|
|
@@ -64,7 +64,7 @@ export function ReturnReceiveDialog({
|
|
|
64
64
|
<DialogBody className="flex flex-col gap-4">
|
|
65
65
|
{lines.map((line) => (
|
|
66
66
|
<div key={line.id} className="flex flex-col gap-3">
|
|
67
|
-
<span className="font-medium text-
|
|
67
|
+
<span className="font-medium text-base">{line.label}</span>
|
|
68
68
|
<div className="flex flex-col gap-3">
|
|
69
69
|
<Field>
|
|
70
70
|
<FieldLabel htmlFor={`qty-${line.id}`}>
|
|
@@ -193,7 +193,7 @@ export function ReturnRefundDialog({
|
|
|
193
193
|
</DialogHeader>
|
|
194
194
|
<DialogBody className="flex flex-col gap-4">
|
|
195
195
|
{owesNothing ? (
|
|
196
|
-
<p className="text-muted-foreground text-
|
|
196
|
+
<p className="text-muted-foreground text-base">
|
|
197
197
|
{t('admin.pages.orders.detail.returns.nothing_owed')}
|
|
198
198
|
</p>
|
|
199
199
|
) : (
|
|
@@ -35,7 +35,9 @@ export function SearchInput({
|
|
|
35
35
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
|
-
|
|
38
|
+
// The outline button's edge rather than a form field's, so the search box
|
|
39
|
+
// reads as one of the toolbar's controls beside the buttons it sits with.
|
|
40
|
+
<InputGroup className={cn('border-border/80', className)}>
|
|
39
41
|
<InputGroupAddon>
|
|
40
42
|
<SearchIcon className="size-4" />
|
|
41
43
|
</InputGroupAddon>
|
|
@@ -70,7 +70,7 @@ export function SecretInput({
|
|
|
70
70
|
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
71
71
|
<div className="flex items-center gap-2">
|
|
72
72
|
<div className="flex min-h-8 w-full min-w-0 items-center rounded-lg border border-border bg-muted/40 px-2.5 py-1.5">
|
|
73
|
-
<span className="font-mono text-
|
|
73
|
+
<span className="font-mono text-base tabular-nums text-muted-foreground" id={id}>
|
|
74
74
|
{storedMask}
|
|
75
75
|
</span>
|
|
76
76
|
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
import { cn } from '../lib/utils'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A titled group of cards on a page — an order's fulfillments, its returns.
|
|
6
|
+
* The heading sits on the page with no frame of its own, and each record
|
|
7
|
+
* below it is a card: a card inside a card would box the records twice.
|
|
8
|
+
*/
|
|
9
|
+
function Section({ className, ...props }: React.ComponentProps<'section'>) {
|
|
10
|
+
return (
|
|
11
|
+
<section
|
|
12
|
+
data-slot="section"
|
|
13
|
+
className={cn('flex min-w-0 flex-col gap-3', className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The heading row: a `SectionTitle`, then `SectionActions` at the end. */
|
|
20
|
+
function SectionHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
data-slot="section-header"
|
|
24
|
+
className={cn('flex min-h-8 flex-wrap items-center gap-2', className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A leading icon is set in a tile, so the heading reads as one without a frame. */
|
|
31
|
+
function SectionTitle({ className, ...props }: React.ComponentProps<'h2'>) {
|
|
32
|
+
return (
|
|
33
|
+
<h2
|
|
34
|
+
data-slot="section-title"
|
|
35
|
+
className={cn(
|
|
36
|
+
'flex min-w-0 flex-1 items-center gap-2 font-semibold text-base',
|
|
37
|
+
'[&>svg:first-child]:box-content [&>svg:first-child]:size-4 [&>svg:first-child]:shrink-0 [&>svg:first-child]:rounded-lg [&>svg:first-child]:bg-accent-strong [&>svg:first-child]:p-1.5',
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SectionActions({ className, ...props }: React.ComponentProps<'div'>) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-slot="section-actions"
|
|
49
|
+
className={cn('flex shrink-0 items-center gap-2', className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Section, SectionActions, SectionHeader, SectionTitle }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../lib/utils'
|
|
3
|
+
import { Item, ItemContent, ItemDescription, ItemTitle } from '../ui/item'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One figure in a row of headline numbers at the top of a record page: a
|
|
7
|
+
* small label over a large value, each in its own outline item. Lay several
|
|
8
|
+
* out in a grid rather than inside a card.
|
|
9
|
+
*/
|
|
10
|
+
export function StatItem({
|
|
11
|
+
label,
|
|
12
|
+
value,
|
|
13
|
+
className,
|
|
14
|
+
}: {
|
|
15
|
+
label: ReactNode
|
|
16
|
+
value: ReactNode
|
|
17
|
+
className?: string
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<Item variant="outline" className={cn('items-start', className)}>
|
|
21
|
+
<ItemContent>
|
|
22
|
+
<ItemDescription>{label}</ItemDescription>
|
|
23
|
+
<ItemTitle className="text-lg font-semibold">{value}</ItemTitle>
|
|
24
|
+
</ItemContent>
|
|
25
|
+
</Item>
|
|
26
|
+
)
|
|
27
|
+
}
|