@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.
Files changed (84) hide show
  1. package/package.json +20 -19
  2. package/src/index.ts +5 -0
  3. package/src/lib/field-trigger.ts +10 -0
  4. package/src/lib/same-rich-text.ts +12 -3
  5. package/src/lib/utils.ts +14 -1
  6. package/src/spree/address-block.tsx +3 -3
  7. package/src/spree/address-book-row.tsx +88 -0
  8. package/src/spree/bulk-price-table.tsx +3 -3
  9. package/src/spree/claim-resolve-dialog.tsx +1 -1
  10. package/src/spree/collapsible-card.tsx +2 -2
  11. package/src/spree/color-picker.tsx +1 -1
  12. package/src/spree/currency-label.tsx +71 -0
  13. package/src/spree/data-grid/cells.tsx +2 -2
  14. package/src/spree/data-grid/data-grid.tsx +2 -2
  15. package/src/spree/delivery-form-dialog.tsx +1 -1
  16. package/src/spree/drag-handle.tsx +1 -1
  17. package/src/spree/editable-note-card.tsx +2 -2
  18. package/src/spree/form-actions.tsx +32 -10
  19. package/src/spree/fulfill-items-form.tsx +20 -22
  20. package/src/spree/fulfillment-edit-dialog.tsx +1 -1
  21. package/src/spree/fulfillment-item-list.tsx +87 -28
  22. package/src/spree/fulfillment-panel.tsx +2 -2
  23. package/src/spree/icons.ts +8 -0
  24. package/src/spree/json-preview-drawer.tsx +32 -7
  25. package/src/spree/json-value-view.tsx +14 -4
  26. package/src/spree/label-upload-dialog.tsx +1 -1
  27. package/src/spree/language-menu-items.tsx +1 -1
  28. package/src/spree/metadata/metadata-card.tsx +2 -2
  29. package/src/spree/order-cancel-dialog.tsx +3 -3
  30. package/src/spree/post-sale-create-dialogs.tsx +36 -33
  31. package/src/spree/post-sale-fields.tsx +30 -1
  32. package/src/spree/product-identity.tsx +74 -0
  33. package/src/spree/product-membership-list.tsx +79 -41
  34. package/src/spree/products/status-card.tsx +1 -1
  35. package/src/spree/quantity-tier-editor.tsx +1 -1
  36. package/src/spree/return-dialogs.tsx +2 -2
  37. package/src/spree/search-input.tsx +3 -1
  38. package/src/spree/secret-input.tsx +1 -1
  39. package/src/spree/section.tsx +55 -0
  40. package/src/spree/stat-item.tsx +27 -0
  41. package/src/spree/tax-identifiers-card.tsx +117 -76
  42. package/src/spree/theme-toggle.tsx +1 -1
  43. package/src/spree/wizard.tsx +2 -2
  44. package/src/styles.css +226 -120
  45. package/src/ui/alert.tsx +20 -17
  46. package/src/ui/attachment.tsx +1 -1
  47. package/src/ui/avatar.tsx +2 -2
  48. package/src/ui/badge.tsx +1 -1
  49. package/src/ui/breadcrumb.tsx +1 -1
  50. package/src/ui/button.tsx +14 -11
  51. package/src/ui/calendar.tsx +2 -2
  52. package/src/ui/card.tsx +14 -37
  53. package/src/ui/chart.tsx +1 -1
  54. package/src/ui/checkbox.tsx +2 -2
  55. package/src/ui/code-editor.tsx +6 -2
  56. package/src/ui/collapsible.tsx +21 -2
  57. package/src/ui/combobox.tsx +12 -16
  58. package/src/ui/command.tsx +8 -6
  59. package/src/ui/context-menu.tsx +1 -1
  60. package/src/ui/data-table.tsx +58 -15
  61. package/src/ui/date-picker.tsx +3 -3
  62. package/src/ui/date-range-panel.tsx +102 -0
  63. package/src/ui/date-range-picker.tsx +33 -72
  64. package/src/ui/dialog.tsx +3 -3
  65. package/src/ui/dropdown-menu.tsx +6 -6
  66. package/src/ui/empty.tsx +3 -3
  67. package/src/ui/field.tsx +33 -12
  68. package/src/ui/input-group.tsx +17 -6
  69. package/src/ui/input.tsx +148 -4
  70. package/src/ui/item.tsx +7 -4
  71. package/src/ui/label.tsx +1 -1
  72. package/src/ui/media-preview.tsx +40 -5
  73. package/src/ui/pagination.tsx +16 -4
  74. package/src/ui/progress.tsx +2 -2
  75. package/src/ui/radio-group.tsx +2 -2
  76. package/src/ui/rich-text-editor.tsx +3 -3
  77. package/src/ui/select.tsx +7 -16
  78. package/src/ui/sheet.tsx +13 -5
  79. package/src/ui/sidebar.tsx +5 -5
  80. package/src/ui/switch.tsx +8 -2
  81. package/src/ui/tabs.tsx +45 -9
  82. package/src/ui/textarea.tsx +1 -1
  83. package/src/ui/toast.tsx +2 -2
  84. package/src/ui/tooltip.tsx +1 -1
package/src/ui/alert.tsx CHANGED
@@ -4,23 +4,22 @@ import * as React from 'react'
4
4
  import { cn } from '../lib/utils'
5
5
 
6
6
  const alertVariants = cva(
7
- "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
7
+ "group/alert relative grid w-full gap-0.5 rounded-card px-3.5 py-3 text-left text-base has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-20 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
8
8
  {
9
9
  variants: {
10
10
  variant: {
11
- default: 'bg-card text-card-foreground',
12
- destructive:
13
- 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
14
- // Custom colours, per the shadcn recipe: a tinted fill with matching
15
- // text rather than the card background, so an advisory note reads as
16
- // information instead of as a card of its own. Mirrors the `success`
17
- // recipe in badge.tsx, including the dark-mode text bump for legibility.
18
- info: 'border-blue-500/20 bg-blue-500/10 text-blue-800 *:data-[slot=alert-description]:text-blue-800/90 dark:text-blue-300 dark:*:data-[slot=alert-description]:text-blue-300/90',
19
- // Amber rather than the `--color-warning` token, which is yellow-500 —
20
- // a fill colour too light to read as body text. Amber is what the rest
21
- // of the dashboard already uses to mark a warning.
22
- warning:
23
- 'border-amber-500/20 bg-amber-500/10 text-amber-800 *:data-[slot=alert-description]:text-amber-800/90 dark:text-amber-300 dark:*:data-[slot=alert-description]:text-amber-300/90',
11
+ // Borderless: the fill alone sets an alert apart. A plain note takes a
12
+ // card's fill and lift, so it sits among the cards around it.
13
+ default: 'bg-card text-card-foreground shadow-card-lift',
14
+ // The status variants read off the badges' status ramps — their text
15
+ // step over the ramp's quieter `surface` fill, which suits a block of
16
+ // copy. Solid colours rather than one hue at an opacity, which
17
+ // takes on whatever sits behind it and washes out on a dark card. The
18
+ // description keeps the variant's colour, a little softer than the
19
+ // title.
20
+ destructive: 'bg-danger-surface text-danger *:data-[slot=alert-description]:text-danger/85',
21
+ info: 'bg-info-surface text-info *:data-[slot=alert-description]:text-info/85',
22
+ warning: 'bg-warning-surface text-warning *:data-[slot=alert-description]:text-warning/85',
24
23
  },
25
24
  },
26
25
  defaultVariants: {
@@ -49,7 +48,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
49
48
  <div
50
49
  data-slot="alert-title"
51
50
  className={cn(
52
- 'font-semibold group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
51
+ 'font-semibold group-has-[>svg]/alert:col-start-2 [&_a]:text-link [&_a]:transition-colors [&_a]:hover:text-link-hover',
53
52
  className,
54
53
  )}
55
54
  {...props}
@@ -62,7 +61,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>)
62
61
  <div
63
62
  data-slot="alert-description"
64
63
  className={cn(
65
- 'text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
64
+ 'text-base text-balance text-muted-foreground md:text-pretty [&_a]:text-link [&_a]:transition-colors [&_a]:hover:text-link-hover [&_p:not(:last-child)]:mb-4',
66
65
  className,
67
66
  )}
68
67
  {...props}
@@ -72,7 +71,11 @@ function AlertDescription({ className, ...props }: React.ComponentProps<'div'>)
72
71
 
73
72
  function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
74
73
  return (
75
- <div data-slot="alert-action" className={cn('absolute top-2 right-2', className)} {...props} />
74
+ <div
75
+ data-slot="alert-action"
76
+ className={cn('absolute top-2.5 right-3', className)}
77
+ {...props}
78
+ />
76
79
  )
77
80
  }
78
81
 
@@ -96,7 +96,7 @@ function AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>)
96
96
  return (
97
97
  <span
98
98
  data-slot="attachment-title"
99
- className={cn('block max-w-full min-w-0 truncate font-medium text-sm', className)}
99
+ className={cn('block max-w-full min-w-0 truncate font-medium text-base', className)}
100
100
  {...props}
101
101
  />
102
102
  )
package/src/ui/avatar.tsx CHANGED
@@ -41,7 +41,7 @@ function AvatarFallback({
41
41
  <AvatarPrimitive.Fallback
42
42
  data-slot="avatar-fallback"
43
43
  className={cn(
44
- 'flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs uppercase',
44
+ 'flex size-full items-center justify-center rounded-full bg-muted text-base text-muted-foreground group-data-[size=sm]/avatar:text-xs uppercase',
45
45
  className,
46
46
  )}
47
47
  {...props}
@@ -83,7 +83,7 @@ function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>)
83
83
  <div
84
84
  data-slot="avatar-group-count"
85
85
  className={cn(
86
- 'relative flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
86
+ 'relative flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-base text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
87
87
  className,
88
88
  )}
89
89
  {...props}
package/src/ui/badge.tsx CHANGED
@@ -272,7 +272,7 @@ function StatusBadge({
272
272
  data-slot="status-badge"
273
273
  data-status={status}
274
274
  className={cn(
275
- 'inline-flex w-fit shrink-0 items-center gap-1.5 whitespace-nowrap text-sm capitalize',
275
+ 'inline-flex w-fit shrink-0 items-center gap-1.5 whitespace-nowrap text-base capitalize',
276
276
  className,
277
277
  )}
278
278
  >
@@ -20,7 +20,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
20
20
  <ol
21
21
  data-slot="breadcrumb-list"
22
22
  className={cn(
23
- 'flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground',
23
+ 'flex flex-wrap items-center gap-1.5 text-base wrap-break-word text-muted-foreground',
24
24
  className,
25
25
  )}
26
26
  {...props}
package/src/ui/button.tsx CHANGED
@@ -4,23 +4,26 @@ import { cn } from '../lib/utils'
4
4
  import { Slot } from './slot'
5
5
 
6
6
  const buttonVariants = cva(
7
- "group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-lg border border-transparent text-sm font-medium whitespace-nowrap cursor-pointer select-none no-underline transition-[color,background-color,border-color,box-shadow,transform,translate,scale] duration-100 ease-out outline-none active:scale-[0.98] motion-reduce:active:scale-100 focus-visible:border-blue-500 focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
7
+ "group/button inline-flex shrink-0 items-center justify-center gap-2 rounded-xl border border-transparent text-base font-medium whitespace-nowrap cursor-pointer select-none no-underline transition-[color,background-color,border-color,box-shadow,transform,translate,scale] duration-100 ease-out outline-none active:scale-[0.98] motion-reduce:active:scale-100 focus-visible:border-blue-500 focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] disabled:pointer-events-none disabled:opacity-70 disabled:cursor-not-allowed aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
8
  {
9
9
  variants: {
10
10
  variant: {
11
- // `shadow-xs` like the outline and destructive variants: the primary
12
- // action is the one thing on a page that should sit slightly above it,
13
- // and it was the only variant with no lift at all.
14
- default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/85',
11
+ // Buttons with chrome (primary, outline, destructive) take the field's
12
+ // `shadow-control`, so a button and the field beside it share one lift.
13
+ // Disabled drops the fill rather than fading it: a faded black (or, in
14
+ // dark mode, white) pill still reads as the page's live primary action.
15
+ default:
16
+ 'bg-primary text-primary-foreground shadow-control hover:bg-primary/85 disabled:border-border disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100 disabled:shadow-none',
15
17
  outline:
16
- 'border-border/80 bg-card shadow-xs text-foreground hover:bg-accent hover:text-foreground aria-expanded:bg-accent',
18
+ 'border-border/80 bg-card shadow-control text-foreground hover:bg-accent hover:text-foreground aria-expanded:bg-accent',
17
19
  // `group-hover/row:` — inside a hovered table row the row already wears
18
20
  // --accent, so repeating it here leaves the button at 1.03:1 against its
19
21
  // own background: invisible exactly when the pointer is on it. On a
20
22
  // hovered row it steps to --accent-strong instead.
21
23
  ghost:
22
24
  'hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent group-hover/row:hover:bg-accent-strong',
23
- destructive: 'text-destructive bg-card border-border shadow-xs hover:bg-destructive/10',
25
+ destructive:
26
+ 'text-destructive bg-card border-border shadow-control hover:bg-destructive/10',
24
27
  // Ghost's chrome (none until hover) with destructive's colour. The
25
28
  // inline remove buttons that sit inside a row or field want no box of
26
29
  // their own, but still need the red tint on hover rather than ghost's
@@ -30,12 +33,12 @@ const buttonVariants = cva(
30
33
  },
31
34
  size: {
32
35
  default: 'h-9 px-3 py-1.5',
33
- xs: "h-6 gap-1 rounded-md px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
34
- sm: "h-7 gap-1 rounded-md px-2 py-1 text-sm [&_svg:not([class*='size-'])]:size-3.5",
36
+ xs: "h-6 gap-1 rounded-lg px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
37
+ sm: "h-7 gap-1 rounded-lg px-2 py-1 text-base [&_svg:not([class*='size-'])]:size-3.5",
35
38
  lg: 'h-10 px-3 py-2 text-base',
36
39
  icon: 'size-9',
37
- 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
38
- 'icon-sm': 'size-7 rounded-md',
40
+ 'icon-xs': "size-6 rounded-lg [&_svg:not([class*='size-'])]:size-3",
41
+ 'icon-sm': 'size-7 rounded-lg',
39
42
  'icon-lg': 'size-10',
40
43
  },
41
44
  },
@@ -22,7 +22,7 @@ function Calendar({
22
22
  months: 'flex flex-col sm:flex-row gap-2 relative',
23
23
  month: 'flex flex-col gap-4',
24
24
  month_caption: 'flex justify-center pt-1 relative items-center w-full',
25
- caption_label: 'text-sm font-medium',
25
+ caption_label: 'text-base font-medium',
26
26
  nav: 'absolute top-1 inset-x-0 flex items-center justify-between px-1 z-10',
27
27
  button_previous:
28
28
  'inline-flex size-7 items-center justify-center rounded-md border border-border bg-transparent p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground',
@@ -32,7 +32,7 @@ function Calendar({
32
32
  weekdays: 'flex',
33
33
  weekday: 'text-muted-foreground rounded-md w-8 font-normal text-xs',
34
34
  week: 'flex w-full mt-2',
35
- day: 'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-blue-50 [&:has([aria-selected].day-outside)]:bg-blue-50/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
35
+ day: 'relative p-0 text-center text-base focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-blue-50 [&:has([aria-selected].day-outside)]:bg-blue-50/50 [&:has([aria-selected].day-range-end)]:rounded-r-md',
36
36
  day_button: cn(
37
37
  'inline-flex size-8 items-center justify-center rounded-md p-0 font-normal transition-colors',
38
38
  'hover:bg-accent hover:text-foreground',
package/src/ui/card.tsx CHANGED
@@ -4,48 +4,29 @@ import { cn } from '../lib/utils'
4
4
 
5
5
  function Card({
6
6
  className,
7
- variant = 'default',
7
+ variant: _variant,
8
8
  ...props
9
9
  }: React.ComponentProps<'div'> & {
10
10
  /**
11
- * `nested` is one resource listed inside a card — a fulfillment on an order,
12
- * a return, a rule. It sits on a recessed surface so it reads as a thing
13
- * *within* the card rather than another card beside it, and carries its own
14
- * horizontal padding so its sections divide edge to edge.
15
- *
16
- * The box is load-bearing once there is more than one of them: a fulfillment
17
- * already divides its own rows, so grouping by rules alone would draw the
18
- * boundary between two fulfillments as the same line at the same inset as
19
- * the boundary between two items — and the panel's own actions would have no
20
- * visible owner.
21
- *
22
- * `container` is the card those nested records sit in. Pair the two: the
23
- * container recedes, the records stay raised. A `default` card wrapping
24
- * `nested` ones puts the content in a well and the frame in front of it,
25
- * which reads as the content being demoted.
11
+ * @deprecated Every card renders the same; the prop is accepted so plugin
12
+ * code that passes it keeps compiling. Group records under a `Section`
13
+ * heading, one plain card each, instead of nesting cards.
26
14
  */
27
15
  variant?: 'default' | 'nested' | 'container'
28
16
  }) {
29
17
  return (
30
18
  <div
31
19
  data-slot="card"
32
- data-variant={variant}
33
20
  className={cn(
34
21
  // `overflow-x-clip`: a wide child (a table sized to its columns) otherwise
35
22
  // propagates its width up through every `overflow: visible` ancestor to
36
23
  // the document, laying the whole page out wider than the viewport. `clip`
37
24
  // rather than `hidden` so `overflow-y` stays `visible` and sticky
38
25
  // descendants keep resolving against the page.
39
- 'group/card flex flex-col min-w-0 overflow-x-clip break-words text-card-foreground shadow-xs',
40
- variant === 'nested' &&
41
- 'rounded-lg border border-border-subtle bg-nested-raised px-3 shadow-none',
42
- // Holds nested records rather than content, so it recedes and lets
43
- // them be the raised, readable thing.
44
- variant === 'container' && 'rounded-xl border border-border-card bg-card-container',
45
- // `--border-card`, not `--border`: a card is a region of the sheet
46
- // rather than a surface floating above it, so its edge is softer than
47
- // the structural one the sheet, dialogs and dropdowns draw.
48
- variant === 'default' && 'rounded-xl border border-border-card bg-card',
26
+ 'group/card flex flex-col min-w-0 overflow-x-clip break-words text-card-foreground',
27
+ // A faint border and a wide, soft lift (`--card-shadow`; none in
28
+ // dark mode).
29
+ 'rounded-card border border-card-edge bg-card shadow-card-lift',
49
30
  className,
50
31
  )}
51
32
  {...props}
@@ -58,7 +39,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
58
39
  <div
59
40
  data-slot="card-header"
60
41
  className={cn(
61
- 'group/card-header @container/card-header grid auto-rows-min items-start gap-1 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] px-3 py-2.5 border-b border-border-subtle rounded-t-[calc(var(--radius-2xl)-1px)] group-data-[variant=nested]/card:rounded-none group-data-[variant=nested]/card:px-0 group-data-[variant=nested]/card:py-2 group-data-[variant=nested]/card:items-center',
42
+ 'group/card-header @container/card-header grid auto-rows-min items-center gap-1 group-data-[size=sm]/card:px-3.5 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] mx-4 pt-3 pb-2 rounded-t-[calc(var(--radius-card)-1px)]',
62
43
  className,
63
44
  )}
64
45
  {...props}
@@ -82,7 +63,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
82
63
  return (
83
64
  <div
84
65
  data-slot="card-description"
85
- className={cn('text-sm text-muted-foreground text-left', className)}
66
+ className={cn('text-base text-muted-foreground text-left', className)}
86
67
  {...props}
87
68
  />
88
69
  )
@@ -93,13 +74,7 @@ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
93
74
  }
94
75
 
95
76
  function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
96
- return (
97
- <div
98
- data-slot="card-content"
99
- className={cn('p-3 group-data-[variant=nested]/card:px-0', className)}
100
- {...props}
101
- />
102
- )
77
+ return <div data-slot="card-content" className={cn('p-4', className)} {...props} />
103
78
  }
104
79
 
105
80
  function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
@@ -107,7 +82,9 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
107
82
  <div
108
83
  data-slot="card-footer"
109
84
  className={cn(
110
- 'flex items-center p-3 bg-transparent border-t border-border-subtle rounded-b-[calc(var(--radius-xl)-1px)] group-data-[variant=nested]/card:rounded-none group-data-[variant=nested]/card:px-0',
85
+ // Side margin, not padding, so the top rule stops short of the card's
86
+ // edges and reads as a divider inside it.
87
+ 'mx-4 flex items-center py-4 bg-transparent border-t border-border-subtle',
111
88
  className,
112
89
  )}
113
90
  {...props}
package/src/ui/chart.tsx CHANGED
@@ -69,7 +69,7 @@ export function ChartTooltipContent({
69
69
  const formattedLabel = labelFormatter ? labelFormatter(label ?? '') : label
70
70
 
71
71
  return (
72
- <div className={cn('rounded-lg border bg-background px-3 py-2 text-sm shadow-md', className)}>
72
+ <div className={cn('rounded-lg border bg-background px-3 py-2 text-base shadow-md', className)}>
73
73
  <div className="mb-1 font-medium">{formattedLabel}</div>
74
74
  <div className="flex flex-col gap-0.5">
75
75
  {payload.map((item, i) => {
@@ -25,7 +25,7 @@ function Checkbox({
25
25
  disabled={disabled}
26
26
  name={name}
27
27
  className={cn(
28
- 'peer relative inline-block size-4 shrink-0 cursor-pointer rounded-[4px] border border-border shadow-xs bg-card align-[-3px] outline-none transition-[color,background-color,border-color,box-shadow] duration-100 ease-out',
28
+ 'peer relative inline-block size-4 shrink-0 cursor-pointer rounded-[4px] border border-field-border shadow-control bg-card align-[-3px] outline-none transition-[color,background-color,border-color,box-shadow] duration-100 ease-out',
29
29
  // The 16px box is the right *visual* size in a dense table, but on its
30
30
  // own it is a 16px target — under the 24px WCAG 2.2 asks for, on the
31
31
  // control every bulk action starts from. A pseudo-element extends the
@@ -34,7 +34,7 @@ function Checkbox({
34
34
  // bridge, which would otherwise swallow presses in the padded margin
35
35
  // and navigate instead of selecting.
36
36
  "before:absolute before:inset-[-9px] before:z-1 before:content-['']",
37
- 'focus:border-blue-500 focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]',
37
+ 'focus:border-blue-500 focus:shadow-field-focus',
38
38
  'disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground',
39
39
  'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
40
40
  'data-[checked]:border-blue-500 data-[checked]:bg-blue-500 data-[checked]:text-white',
@@ -71,7 +71,11 @@ const theme = EditorView.theme({
71
71
  fontSize: '13px',
72
72
  height: '100%',
73
73
  },
74
- '.cm-scroller': { fontFamily: 'var(--font-mono, ui-monospace, monospace)', lineHeight: '1.6' },
74
+ '.cm-scroller': {
75
+ fontFamily: 'var(--font-mono, ui-monospace, monospace)',
76
+ fontFeatureSettings: 'var(--default-mono-font-feature-settings, normal)',
77
+ lineHeight: '1.6',
78
+ },
75
79
  '.cm-content': { caretColor: 'var(--foreground)' },
76
80
  '.cm-gutters': {
77
81
  backgroundColor: 'var(--background)',
@@ -238,7 +242,7 @@ function CodeEditor({
238
242
  ref={containerRef}
239
243
  data-slot="code-editor"
240
244
  className={cn(
241
- 'min-h-64 overflow-hidden rounded-md border border-border bg-card shadow-control focus-within:border-blue-500 focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]',
245
+ 'min-h-64 overflow-hidden rounded-xl border border-field-border bg-card shadow-control focus-within:border-blue-500 focus-within:shadow-field-focus',
242
246
  className,
243
247
  )}
244
248
  />
@@ -1,5 +1,6 @@
1
1
  import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible'
2
2
  import type * as React from 'react'
3
+ import { cn } from '../lib/utils'
3
4
 
4
5
  function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
5
6
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
@@ -11,8 +12,26 @@ function CollapsibleTrigger({
11
12
  return <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
12
13
  }
13
14
 
14
- function CollapsibleContent({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Panel>) {
15
- return <CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
15
+ /**
16
+ * Opens and closes by sliding its height, which Base UI measures into
17
+ * `--collapsible-panel-height`, and fading so rows aren't sliced by the
18
+ * moving edge. Same timing as the chevrons that toggle it. Instant under
19
+ * reduced motion.
20
+ */
21
+ function CollapsibleContent({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Panel>) {
25
+ return (
26
+ <CollapsiblePrimitive.Panel
27
+ data-slot="collapsible-content"
28
+ className={cn(
29
+ 'h-[var(--collapsible-panel-height)] overflow-hidden transition-[height,opacity] duration-200 ease-out data-[ending-style]:h-0 data-[ending-style]:opacity-0 data-[starting-style]:h-0 data-[starting-style]:opacity-0 motion-reduce:transition-none',
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ )
16
35
  }
17
36
 
18
37
  export { Collapsible, CollapsibleContent, CollapsibleTrigger }
@@ -3,8 +3,9 @@
3
3
  import { Combobox as ComboboxPrimitive } from '@base-ui/react'
4
4
  import i18n from 'i18next'
5
5
  import * as React from 'react'
6
+ import { fieldTriggerClassName } from '../lib/field-trigger'
6
7
  import { cn } from '../lib/utils'
7
- import { CheckIcon, ChevronDownIcon, XIcon } from '../spree/icons'
8
+ import { CheckIcon, ChevronsUpDownIcon, XIcon } from '../spree/icons'
8
9
  import { Button } from './button'
9
10
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './input-group'
10
11
 
@@ -48,7 +49,7 @@ function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Tr
48
49
  {...props}
49
50
  >
50
51
  {children}
51
- <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
52
+ <ChevronsUpDownIcon className="pointer-events-none size-4 text-muted-foreground" />
52
53
  </ComboboxPrimitive.Trigger>
53
54
  )
54
55
  }
@@ -82,14 +83,11 @@ function ComboboxButtonTrigger({
82
83
  <ComboboxPrimitive.Trigger
83
84
  data-slot="combobox-button-trigger"
84
85
  data-size={size}
85
- className={cn(
86
- "group/combobox-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-[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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
87
- className,
88
- )}
86
+ className={cn('group/combobox-trigger', fieldTriggerClassName, className)}
89
87
  {...props}
90
88
  >
91
89
  <span className="flex min-w-0 items-center gap-2 truncate">{children}</span>
92
- <ChevronDownIcon className="pointer-events-none size-4 shrink-0 text-muted-foreground group-data-[size=sm]/combobox-trigger:size-3.5" />
90
+ <ChevronsUpDownIcon className="pointer-events-none size-4 shrink-0 text-muted-foreground group-data-[size=sm]/combobox-trigger:size-3.5" />
93
91
  </ComboboxPrimitive.Trigger>
94
92
  )
95
93
  }
@@ -218,7 +216,7 @@ function ComboboxContent({
218
216
  // picker: the list re-renders as the user filters, and options
219
217
  // arriving mid-transition sit at `opacity-0` long enough to be
220
218
  // unclickable. The keyframes make the panel opaque immediately.
221
- 'group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
219
+ 'group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-xl bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
222
220
  className,
223
221
  )}
224
222
  {...props}
@@ -289,7 +287,7 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
289
287
  <ComboboxPrimitive.Empty
290
288
  data-slot="combobox-empty"
291
289
  className={cn(
292
- 'hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex',
290
+ 'hidden w-full justify-center py-2 text-center text-base text-muted-foreground group-data-empty/combobox-content:flex',
293
291
  className,
294
292
  )}
295
293
  {...props}
@@ -321,7 +319,7 @@ function ComboboxChips({
321
319
  // resize lets the positioner follow the anchor smoothly; making it
322
320
  // instant jumps the open popup out from under the pointer, so the next
323
321
  // option click lands on nothing.
324
- 'flex flex-wrap items-center gap-1.5 rounded-md border border-border shadow-control bg-card text-foreground px-2.5 py-1.5 text-base transition-all duration-100 ease-out focus-within:border-blue-500 focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:p-1.5',
322
+ 'flex flex-wrap items-center gap-1.5 rounded-xl border border-field-border shadow-control bg-card text-foreground px-2.5 py-1.5 text-base transition-all duration-100 ease-out not-focus-within:hover:bg-field-hover focus-within:border-blue-500 focus-within:shadow-field-focus has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:p-1.5',
325
323
  className,
326
324
  )}
327
325
  {...props}
@@ -345,11 +343,9 @@ function ComboboxChip({
345
343
  // was shorter than the remove button inside it, and left the label with
346
344
  // no vertical breathing room at any screen size.
347
345
  //
348
- // Fill matches the highlighted row in the list below (`bg-accent`): a
349
- // chip and a selected option are the same thing at two moments, so they
350
- // read as the same thing. `bg-muted` sat within 1.03:1 of the field it
351
- // sits on, which left the chips barely visible.
352
- 'flex min-h-7 w-fit max-w-full items-center justify-center gap-1 rounded-sm bg-accent py-1 pl-2.5 pr-2 text-sm font-base text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-1',
346
+ // Outlined, so a chip keeps its edge while the field under it takes its
347
+ // hover fill; a fill alone matched that hover and the chip vanished.
348
+ 'flex min-h-7 w-fit max-w-full items-center justify-center gap-1 rounded-sm bg-accent/50 border py-1 pl-2.5 pr-2 text-base font-base text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-1',
353
349
  className,
354
350
  )}
355
351
  {...props}
@@ -358,7 +354,7 @@ function ComboboxChip({
358
354
  {showRemove && (
359
355
  <ComboboxPrimitive.ChipRemove
360
356
  render={<Button variant="ghost" size="icon-sm" />}
361
- className="size-6 shrink-0 opacity-50 hover:opacity-100"
357
+ className="size-6 shrink-0 opacity-50 hover:bg-transparent hover:opacity-100"
362
358
  data-slot="combobox-chip-remove"
363
359
  // Names the chip it clears ("Remove Wholesale") rather than a bare
364
360
  // "Remove": several sit side by side, and a dialog's own submit
@@ -91,10 +91,6 @@ function CommandDialog({
91
91
  }) {
92
92
  return (
93
93
  <Dialog {...props}>
94
- <DialogHeader className="sr-only">
95
- <DialogTitle>{title}</DialogTitle>
96
- <DialogDescription>{description}</DialogDescription>
97
- </DialogHeader>
98
94
  <DialogContent
99
95
  // `-translate-x-1/2` must be restated alongside `translate-y-0`: in
100
96
  // Tailwind v4 both compile to the standalone `translate` property, so
@@ -106,6 +102,12 @@ function CommandDialog({
106
102
  )}
107
103
  showCloseButton={showCloseButton}
108
104
  >
105
+ {/* Inside the popup: the dialog is named by a title within it, and a
106
+ header beside it would stay in the page while the dialog is shut. */}
107
+ <DialogHeader className="sr-only">
108
+ <DialogTitle>{title}</DialogTitle>
109
+ <DialogDescription>{description}</DialogDescription>
110
+ </DialogHeader>
109
111
  {children}
110
112
  </DialogContent>
111
113
  </Dialog>
@@ -199,7 +201,7 @@ function CommandEmpty({ className, ...props }: React.ComponentProps<'div'>) {
199
201
  <div
200
202
  data-slot="command-empty"
201
203
  className={cn(
202
- 'py-10 text-center text-sm text-muted-foreground group-has-[[data-slot=command-item]]/command-list:hidden',
204
+ 'py-10 text-center text-base text-muted-foreground group-has-[[data-slot=command-item]]/command-list:hidden',
203
205
  className,
204
206
  )}
205
207
  {...props}
@@ -278,7 +280,7 @@ function CommandItem({
278
280
  value={value}
279
281
  onClick={() => onSelect?.(text)}
280
282
  className={cn(
281
- "group/command-item relative flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent-strong data-highlighted:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:*:[svg]:text-foreground",
283
+ "group/command-item relative flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 text-base outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent-strong data-highlighted:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:*:[svg]:text-foreground",
282
284
  className,
283
285
  )}
284
286
  {...props}
@@ -68,7 +68,7 @@ function ContextMenuItem({
68
68
  data-inset={inset}
69
69
  data-variant={variant}
70
70
  className={cn(
71
- "group/context-menu-item relative flex cursor-pointer items-center gap-3 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none transition-colors duration-75 focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
71
+ "group/context-menu-item relative flex cursor-pointer items-center gap-3 rounded-lg px-2 py-1.5 text-base outline-hidden select-none transition-colors duration-75 focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
72
72
  className,
73
73
  )}
74
74
  {...props}