@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.
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/styles.css CHANGED
@@ -1,7 +1,7 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
  @import "./shadcn.css";
4
- @import "@fontsource-variable/geist";
4
+ @import "@fontsource-variable/inter/opsz.css";
5
5
  @import "@fontsource-variable/geist-mono";
6
6
 
7
7
  /* Tailwind v4 only scans the consuming project's own source by default.
@@ -31,38 +31,24 @@
31
31
 
32
32
  /* Brand palette.
33
33
  *
34
- * The two themes take different neutral families, for the reason each one's
35
- * large areas need.
34
+ * Both themes use Tailwind's `neutral` ramp: a true zero-chroma grey, so the
35
+ * blue accent stays the only thing in the palette that reads as colour, and
36
+ * as interaction.
36
37
  *
37
- * Light mode is Tailwind's `stone` ramp — the same lightness ladder as
38
- * `neutral`, carrying a trace of warm chroma (0.001–0.013) rather than none.
39
- * Far too little to read as a colour: it stops large white and grey areas
40
- * looking blue-cold, and leaves the blue accent still the only thing in the
41
- * palette that reads as interaction.
42
- *
43
- * Dark mode is a true zero-chroma grey (Geist's own neutral, and shadcn's
44
- * default). The warm tint that flatters a white sheet works against a
45
- * near-black one: at these lightnesses the same chroma stops reading as
46
- * warmth and starts reading as a brown cast over the whole screen. Keep dark
47
- * neutrals at chroma 0 — the hue channel there is noise, not decoration.
48
- *
49
- * In both themes the lightness decisions below are load-bearing for contrast,
50
- * and none of them move. Light mode's values are stone's chroma and hue
51
- * interpolated onto those lightnesses rather than snapped to the nearest ramp
52
- * step; dark mode's are the same lightnesses with the chroma dropped.
38
+ * The lightness decisions below are load-bearing for contrast, and none of
39
+ * them move. Where a ramp step could not meet a contrast requirement, the
40
+ * value sits between two steps at chroma 0 rather than snapping to one.
53
41
  *
54
42
  * Primary CTAs stay near-black rather than adopting the site's blue button:
55
43
  * in a dense admin UI the blue has to stay meaningful as a selection and
56
44
  * focus signal, which it stops being once every page renders a saturated
57
45
  * blue button. */
58
46
  :root {
59
- /* Light-mode neutrals are Tailwind's **stone** scale — a warm grey. Every
60
- neutral here is either an exact stone step (labelled with it) or, where a
61
- step could not meet a contrast requirement, a value on stone's own hue so
62
- it belongs to the same family. Never mix another warm-neutral ramp (zinc,
63
- slate, neutral) into THIS block: two families read as a colour cast on
64
- whichever surface is the odd one out. Dark mode deliberately uses a
65
- different family — see the note on `.dark`.
47
+ /* Light-mode neutrals are Tailwind's **neutral** scale. Every neutral here
48
+ is either an exact step (labelled with it) or, where a step could not
49
+ meet a contrast requirement, a chroma-0 value between two steps. Never
50
+ mix a tinted ramp (stone, zinc, slate) in: two families read as a colour
51
+ cast on whichever surface is the odd one out.
66
52
 
67
53
  Tokens that must equal another token point at it with var() rather than
68
54
  repeating its value, so the two cannot drift. */
@@ -74,28 +60,28 @@
74
60
  --link: oklch(0.5106 0.1868 257.7);
75
61
  --link-hover: oklch(0.44 0.16 257.7);
76
62
 
77
- --background: oklch(98.5% 0.001 106.423); /* stone-50 — chrome one step below white */
78
- --foreground: oklch(14.7% 0.004 49.25); /* stone-950 */
63
+ --background: oklch(98.5% 0 0); /* neutral-50 — chrome one step below white */
64
+ --foreground: oklch(14.5% 0 0); /* neutral-950 */
79
65
  --card: oklch(1 0 0); /* #ffffff — cards and inputs read as raised */
80
66
  --card-foreground: var(--foreground);
81
67
  --popover: var(--card);
82
68
  --popover-foreground: var(--foreground);
83
- --primary: var(--foreground); /* stone-950 — near-black primary CTAs */
69
+ --primary: var(--foreground); /* neutral-950 — near-black primary CTAs */
84
70
  --primary-foreground: var(--card);
85
71
  /* Filled surfaces (table headers, badges, secondary buttons) read as a
86
72
  whisper against the white card rather than a distinct band. */
87
- --secondary: oklch(97% 0.001 106.424); /* stone-100 */
73
+ --secondary: oklch(97% 0 0); /* neutral-100 */
88
74
  --secondary-foreground: var(--foreground);
89
75
  --muted: var(--background);
90
- /* Between stone-500 and -600, on stone's hue. Clears 4.5:1 against every
91
- surface this text lands on — white card 5.19:1, page 4.97:1, secondary and
92
- accent 4.75:1 — where the stone-500 step itself measures 4.41:1 on
93
- secondary and fails. Dark mode takes stone-400 for the same reason. */
94
- --muted-foreground: oklch(0.535 0.0127 58.071);
95
- /* Hover/highlight for menu items and table rows, and the same stone-100 as
76
+ /* Between neutral-500 and -600. Clears 4.5:1 against every
77
+ surface this text lands on, the selected one included: white card 5.65:1,
78
+ sidebar 5.18:1, --accent-strong 4.83:1 (a lighter step measured 4.42:1
79
+ there and failed). */
80
+ --muted-foreground: oklch(0.515 0 0);
81
+ /* Hover/highlight for menu items and table rows, and the same neutral-100 as
96
82
  --secondary: a hover and a filled surface are the same weight of "one step
97
83
  off the card", so they are one value rather than two that drift apart.
98
- stone-200 was tried and reads as a pressed state on a white surface.
84
+ neutral-200 was tried and reads as a pressed state on a white surface.
99
85
 
100
86
  It is also the lightest step that keeps --muted-foreground legible on it:
101
87
  the previous bespoke grey measured 4.47:1, just under the 4.5 this file
@@ -109,38 +95,39 @@
109
95
  --accent cannot do this job. It is the hover/fill surface, so it has to
110
96
  stay light enough for --muted-foreground to clear 4.5:1 on it — which
111
97
  leaves it at 1.04:1 against the page, invisible as a selection. Two
112
- different jobs, two tokens. Nothing but near-black text lands on this
113
- one, so it is free to be darker.
98
+ different jobs, two tokens. Muted text and links land on it too (counts
99
+ and hints in a selected row), so --muted-foreground and --link are set
100
+ to clear 4.5:1 here; keep that in mind before darkening it.
114
101
 
115
- Interpolated on stone's hue rather than snapped to a step (like
116
- --accent-hover and --border-control): stone-200 read heavier than a
117
- selection needs to, and stone-100 is --accent itself, which would collapse
102
+ Between two steps rather than snapped to one (like
103
+ --accent-hover): neutral-200 read heavier than a
104
+ selection needs to, and neutral-100 is --accent itself, which would collapse
118
105
  the two tokens into one and leave a hovered item darker than a selected
119
106
  one. This sits between them. */
120
- --accent-strong: oklch(94.7% 0.002 56);
107
+ --accent-strong: oklch(94.7% 0 0);
121
108
  /* Hover, between --accent and --accent-strong so a nav item has somewhere to
122
109
  go on hover *and* somewhere further to go when it is the active one.
123
- Interpolated on stone's hue rather than snapped — deliberate, like
124
- --border-control.
110
+ Between two steps rather than snapped — deliberate, like
111
+ --accent-strong.
125
112
 
126
113
  --accent is too light to be a hover here: on the sidebar's ground (the
127
114
  page colour, not a card) it measures 1.04:1, which is invisible. */
128
- --accent-hover: oklch(95.9% 0.002 56);
115
+ --accent-hover: oklch(95.9% 0 0);
129
116
  /* The active item hovered. It darkens rather than lightening: a selected
130
117
  row that goes paler under the pointer reads as being deselected. Stays one
131
118
  step under --accent-strong, or the two land on the same value and a
132
119
  selected item stops responding to the pointer. */
133
- --accent-strong-hover: oklch(92.3% 0.003 48.717); /* stone-200 */
120
+ --accent-strong-hover: oklch(92.2% 0 0); /* neutral-200 */
134
121
  --destructive: oklch(0.58 0.22 27); /* red-600 */
135
122
  --destructive-foreground: var(--card);
136
123
  /* The full-strength rule colour. Nothing styles with it directly — the two
137
124
  tokens below are both a share of it, so neither compounds the other's
138
125
  reduction.
139
126
 
140
- stone-200. This is the floor for a hairline: anything lighter stops
127
+ neutral-200. This is the floor for a hairline: anything lighter stops
141
128
  holding a container's edge on a white sheet, and the row rules of a long
142
129
  table start to disappear before the frame around them does. */
143
- --border-base: oklch(92.3% 0.003 48.717); /* stone-200 */
130
+ --border-base: oklch(92.2% 0 0); /* neutral-200 */
144
131
  /* A structural edge — the content sheet, a dialog, a dropdown, a popover, a
145
132
  table's frame. These are surfaces that float above the page or divide it,
146
133
  and their edge is what says where one ends and the next begins, so it
@@ -154,6 +141,10 @@
154
141
  suggest where the region ends. Used by `CardContent`'s frame; a dialog or
155
142
  the sheet itself must keep --border, or the shell loses its outline. */
156
143
  --border-card: color-mix(in srgb, var(--border-base) 70%, transparent);
144
+ /* A card's surface: its faint border, and a wide, soft lift under it, so it
145
+ reads as resting on the page rather than only ruled onto it. */
146
+ --card-edge: var(--border-card);
147
+ --card-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.03), 0 4px 16px 0 rgb(0 0 0 / 0.03);
157
148
  /* Dividers *inside* a container — card header/footer splits, dialog and
158
149
  sheet separators, table row rules.
159
150
 
@@ -167,9 +158,7 @@
167
158
  --border-subtle: color-mix(in srgb, var(--border-base) 50%, transparent);
168
159
  /* A mark that has to hold its own against the surface with nothing else
169
160
  drawing it — the bar of a scrollbar, the focus ring the skip link draws.
170
- WCAG 1.4.11 asks 3:1 of exactly that case: 3.25:1 on the white card,
171
- 3.11:1 on the page ground, the lightest step that clears the minimum on
172
- both.
161
+ The standard border at 80%, so it follows --border in both themes.
173
162
 
174
163
  No form control uses this. Fields, selects and comboboxes take `--border`
175
164
  with `--shadow-control`; checkboxes and radios, too small to carry that
@@ -179,7 +168,34 @@
179
168
  `--muted`, so any resting tint either collides with that meaning or (as
180
169
  happened) lands darker than it and inverts the signal outright. On this
181
170
  palette the enabled state has to stay on the card colour. */
182
- --border-control: oklch(86.9% 0.005 56.366);
171
+ --border-control: color-mix(in srgb, var(--border) 80%, transparent);
172
+ /* A field's resting lift (`shadow-control`), and its edge: a neutral-300
173
+ hairline and a soft two-step shadow below it, which together read as a
174
+ box set into the page, its lower edge a touch darker than the rest. The
175
+ dark theme overrides the edge (see there). */
176
+ --field-shadow: 0 1px 2px -1px rgb(0 0 0 / 0.01), 0 3px 4px -1.5px rgb(0 0 0 / 0.03);
177
+ /* Solid rather than a black veil, so the edge keeps one weight over the
178
+ neutral-100 hover instead of darkening. */
179
+ --field-border: oklch(87% 0 0); /* neutral-300 */
180
+ /* A field under the pointer: the outline button's hover, so every control
181
+ answers the pointer the same way. A disabled field is never tinted on
182
+ hover, so the two never meet. */
183
+ --field-hover: var(--accent);
184
+ /* A switch's track when off: a grey dark enough for the white knob to
185
+ stand clear of it. */
186
+ --switch-off: oklch(87% 0 0); /* neutral-300 */
187
+ /* A number field's step buttons, at rest, under the pointer and pressed.
188
+ Rest is one step past the field's own hover fill so the buttons hold
189
+ their shape when the field is hovered. */
190
+ --stepper: oklch(92.2% 0 0); /* neutral-200 */
191
+ --stepper-hover: oklch(87% 0 0); /* neutral-300 */
192
+ --stepper-active: oklch(70.8% 0 0); /* neutral-400 */
193
+ /* A focused field: the blue edge (set on the field's border), a faint halo,
194
+ a soft glow spreading below and a faint one inside, over the field's
195
+ resting shadow. */
196
+ --field-focus-shadow:
197
+ 0 0 0 3px rgb(53 115 246 / 0.06), 0 4px 20px 2px rgb(53 115 246 / 0.06),
198
+ inset 0 0 8px 1px rgb(53 115 246 / 0.05), var(--field-shadow);
183
199
  /* A card that *holds* other records — an order's fulfillments, its returns,
184
200
  its claims — rather than holding content directly.
185
201
 
@@ -204,7 +220,7 @@
204
220
  /* The recessed well a raised thing sits in — a tab strip's track, a
205
221
  segmented control. Paired with --nested-raised: the track always sits one
206
222
  step under it, which is why the two themes name different tokens rather
207
- than sharing one. Light steps down from white to stone-100. */
223
+ than sharing one. Light steps down from white to neutral-100. */
208
224
  --track-recessed: var(--accent);
209
225
  --input: color-mix(in srgb, var(--border-base) 80%, transparent);
210
226
  --ring: var(--brand-blue);
@@ -220,13 +236,18 @@
220
236
  makes the sheet's rounded edge and shadow read at all: when the two were
221
237
  the same colour the inset was invisible. Everything else still points at
222
238
  the page token it matches, so the rest cannot drift. */
223
- --sidebar: oklch(97% 0.001 106.424); /* stone-100 */
224
- --sidebar-foreground: oklch(26.8% 0.007 34.298);
239
+ --sidebar: oklch(97% 0 0); /* neutral-100 */
240
+ --sidebar-foreground: oklch(26.9% 0 0); /* neutral-800 */
225
241
  --sidebar-primary: var(--primary);
226
242
  --sidebar-primary-foreground: var(--primary-foreground);
227
- --sidebar-accent: oklch(92.3% 0.003 48.717);
243
+ --sidebar-accent: oklch(92.2% 0 0);
228
244
  --sidebar-accent-foreground: var(--accent-foreground);;
229
245
  --sidebar-accent-hover: var(--accent-strong);
246
+ /* The current page in the sidebar: one step stronger than --accent-strong,
247
+ which selected rows and tabs share, so where you are reads at a glance.
248
+ Its own pair so those keep theirs (muted text is tuned to --accent-strong). */
249
+ --sidebar-active: oklch(92.2% 0 0); /* neutral-200 */
250
+ --sidebar-active-hover: oklch(90% 0 0);
230
251
  /* Matches the rule the top bar draws, so every edge framing the shell —
231
252
  the sidebar's, the top bar's, the settings header's — is one weight. */
232
253
  --sidebar-border: color-mix(in srgb, var(--border) 75%, transparent);
@@ -269,32 +290,37 @@
269
290
  --status-blue-bg: var(--color-sky-100);
270
291
  --status-blue-border: var(--color-sky-200);
271
292
  --status-blue-fg: var(--color-sky-700);
293
+
294
+ /* A quieter fill for a status surface larger than a badge — an alert, a
295
+ notice. A paragraph of tinted text over the badge's fill reads as a shout;
296
+ one step lighter holds the same hue at a volume a block of copy can
297
+ carry. Text stays on the badge's step. */
298
+ --status-green-surface: var(--color-green-50);
299
+ --status-amber-surface: var(--color-amber-50);
300
+ --status-red-surface: var(--color-red-50);
301
+ --status-blue-surface: var(--color-sky-50);
272
302
  }
273
303
 
274
304
  .dark {
275
- /* Vercel-style "true black" base, with surfaces stepping up in lightness.
305
+ /* A near-black base, with surfaces stepping up in lightness.
276
306
  *
277
- * Every neutral below is zero-chroma — a true grey, not light mode's warm
278
- * stone. See the palette note at the top of this file for why the two
279
- * themes split. The lightness ladder is unchanged from the warm version;
280
- * only the tint was removed.
307
+ * Every neutral below is zero-chroma, like light mode's. See the palette
308
+ * note at the top of this file.
281
309
  *
282
- * Dark surfaces step in roughly 4-5 points of OKLCH lightness, not the 1-2
283
- * light mode uses. Near-black compresses perceived difference, so a step
284
- * that reads clearly on white disappears down here: an earlier version of
285
- * this ramp had sidebar, muted, background and card inside a 3.5-point band
286
- * and the whole middle of the screen read as one flat surface, which in turn
287
- * made the one boundary that was visible — the sidebar's — look like the
288
- * sheet had been lit. Reference dark dashboards space adjacent surfaces
289
- * 5-6 points apart for the same reason.
310
+ * A near-black ground (#0a0a0a) with surfaces stepping about 2-3 points of
311
+ * OKLCH lightness, as in the near-black dashboards this follows. A grey
312
+ * ground lifted to 22 read as washed out next to them. Cards share the
313
+ * sheet's colour and are told apart by their faint border; the sheet sits
314
+ * one step above the sidebar.
290
315
  *
291
- * The ladder, ground upward: sidebar 14 / muted + background 17 / card 21.6
292
- * / secondary + nested-raised 25 / accent 29.5 / accent-hover 33 /
293
- * accent-strong 38. --card is the anchor, since the inset sheet is the
294
- * surface a merchant actually reads; keep the gaps if you move anything. */
295
- --background: oklch(17% 0 0);
316
+ * The ladder, ground upward: sidebar + muted + background 14.5 / card 16.5
317
+ * / secondary + nested-raised 20 / accent 22.5 / accent-hover 25 /
318
+ * accent-strong 29. --card is the anchor, since the inset sheet is the
319
+ * surface a merchant actually reads. Muted text and links have to clear
320
+ * 4.5:1 on --accent-strong, the lightest surface they sit on. */
321
+ --background: oklch(14.5% 0 0);
296
322
  --foreground: oklch(95% 0 0);
297
- --card: oklch(21.6% 0 0);
323
+ --card: oklch(16.5% 0 0);
298
324
  --card-foreground: var(--foreground);
299
325
  --popover: var(--card);
300
326
  --popover-foreground: var(--foreground);
@@ -307,36 +333,54 @@
307
333
  at or under the card's lightness erases them — while too big a step reads
308
334
  as a bright band across a full-width table header. Dark mode raises where
309
335
  light mode lowers, since a surface reads as raised by getting lighter. */
310
- --secondary: oklch(25% 0 0);
336
+ --secondary: oklch(20% 0 0);
311
337
  --secondary-foreground: var(--foreground);
312
- --muted: oklch(17% 0 0);
313
- /* At stone-400's lightness. The old value failed 4.5:1 on --accent
314
- (4.20:1); this clears every dark ground, the accent included. */
315
- --muted-foreground: oklch(70.9% 0 0);
316
- --accent: oklch(29.5% 0 0);
338
+ --muted: oklch(14.5% 0 0);
339
+ /* Light enough to clear 4.5:1 on every dark ground, the selected
340
+ --accent-strong included (about 5.7:1). Lower than white-ish greys so
341
+ secondary text stays secondary on the near-black ground. */
342
+ --muted-foreground: oklch(72% 0 0);
343
+ --accent: oklch(22.5% 0 0);
317
344
  --accent-foreground: var(--foreground);
318
- --accent-strong: oklch(38% 0 0);
319
- --accent-hover: oklch(33% 0 0);
320
- --accent-strong-hover: oklch(44.4% 0 0);
345
+ --accent-strong: oklch(29% 0 0);
346
+ --accent-hover: oklch(25% 0 0);
347
+ --accent-strong-hover: oklch(34% 0 0);
321
348
  --destructive: oklch(0.704 0.191 22.216);
322
349
  /* A white veil over whatever surface it crosses, so one value works on the
323
350
  page ground and the raised sheet alike. Both shares below are half of it,
324
351
  as in light mode, so a container's edge and the rules inside it are one
325
352
  hairline weight. */
326
- --border-base: oklch(1 0 0 / 16%);
353
+ --border-base: oklch(1 0 0 / 10%);
327
354
  --border: var(--border-base);
328
- --border-card: color-mix(in srgb, var(--border-base) 70%, transparent);
355
+ /* Full strength here, where light mode takes 70%: cards and the sheet they
356
+ sit on share --card, so in the dark the edge is the only thing that tells
357
+ one card from the next. The base veil is kept faint (about 1.25:1 on the
358
+ card) so that edge outlines a card without drawing a box around it. */
359
+ --border-card: var(--border-base);
360
+ /* A black shadow does nothing on a near-black ground; the border is what
361
+ tells one card from the next here. */
362
+ --card-edge: var(--border-card);
363
+ --card-shadow: none;
329
364
  --border-subtle: color-mix(in srgb, var(--border-base) 50%, transparent);
330
- /* Light mode darkens the control edge away from the card; dark mode lightens
331
- it. Not the light value inverted — measured on this theme's own surfaces:
332
- 3.22:1 on the card, 3.34:1 on the page ground. */
333
- --border-control: oklch(52% 0 0);
365
+ --border-control: color-mix(in srgb, var(--border) 80%, transparent);
366
+ /* On a dark card a field's edge does the work a shadow does in light mode:
367
+ a black lift barely shows here, and a firmer one read as a smudge. So the
368
+ field border is a step stronger than a card's (16% white against 10%). */
369
+ --field-border: oklch(1 0 0 / 16%);
370
+ --field-hover: var(--accent);
371
+ --switch-off: oklch(1 0 0 / 24%);
372
+ --stepper: oklch(26.9% 0 0); /* neutral-800 */
373
+ --stepper-hover: oklch(37.1% 0 0); /* neutral-700 */
374
+ --stepper-active: oklch(43.9% 0 0); /* neutral-600 */
375
+ /* Light mode's glows vanish on a near-black ground, so the halo and the
376
+ glow below are firmer here. */
377
+ --field-focus-shadow: 0 0 0 3px rgb(53 115 246 / 0.2), 0 4px 20px 2px rgb(53 115 246 / 0.12);
334
378
  /* Same arrangement as light — container at the page colour, records raised
335
- above it. 1.20:1 between them here. Records take `--nested-raised` rather
379
+ above it, a 5.5-point step. Records take `--nested-raised` rather
336
380
  than `--card`: the card token is a step above the page, which is where the
337
381
  container would sit if it had a fill of its own. */
338
382
  --card-container: var(--background);
339
- --nested-raised: oklch(25% 0 0);
383
+ --nested-raised: oklch(20% 0 0);
340
384
  /* Not --accent here: dark mode's accent is lighter than every raised
341
385
  surface, so a shared token would put the selected tab below its own
342
386
  track. --muted is the step under --nested-raised. */
@@ -347,8 +391,8 @@
347
391
  in dark mode drew a grey halo and lost the one signal that says "this is
348
392
  where typing goes". */
349
393
  --ring: oklch(0.68 0.19 258);
350
- --link: oklch(0.7 0.16 258);
351
- --link-hover: oklch(0.78 0.14 258);
394
+ --link: oklch(0.76 0.14 258);
395
+ --link-hover: oklch(0.83 0.12 258);
352
396
  --chart-1: oklch(0.809 0.105 251.813);
353
397
  --chart-2: oklch(0.623 0.214 259.815);
354
398
  --chart-3: oklch(0.546 0.245 262.881);
@@ -360,21 +404,17 @@
360
404
 
361
405
  Measured against --card, not --background: the shell renders the sidebar's
362
406
  `inset` variant, so the sheet beside it is the card surface and that
363
- pairing is what a merchant actually sees.
364
-
365
- The sheet looking "too bright" was never this token being too dark — it
366
- was the four surfaces above it sitting within half a point of each other,
367
- so the sidebar edge was the only visible boundary on the screen and it
368
- carried the whole contrast budget alone. Widening those steps (see the
369
- ramp note on --background) fixes it; this stays low so the ground still
370
- recedes. */
371
- --sidebar: oklch(14% 0 0);
407
+ pairing is what a merchant actually sees. Two points under it: on a
408
+ near-black ground a wider step makes the sheet read as grey. */
409
+ --sidebar: oklch(14.5% 0 0);
372
410
  --sidebar-foreground: var(--foreground);
373
411
  --sidebar-primary: var(--primary);
374
412
  --sidebar-primary-foreground: var(--primary-foreground);
375
413
  --sidebar-accent: var(--accent);
376
414
  --sidebar-accent-foreground: var(--accent-foreground);
377
415
  --sidebar-accent-hover: var(--accent-hover);
416
+ --sidebar-active: oklch(25% 0 0);
417
+ --sidebar-active-hover: oklch(28% 0 0);
378
418
  /* Matches the rule the top bar draws, so every edge framing the shell —
379
419
  the sidebar's, the top bar's, the settings header's — is one weight. */
380
420
  --sidebar-border: color-mix(in srgb, var(--border) 75%, transparent);
@@ -406,21 +446,55 @@
406
446
  --status-blue-bg: var(--color-sky-900);
407
447
  --status-blue-border: var(--color-sky-900);
408
448
  --status-blue-fg: var(--color-sky-300);
449
+
450
+ /* The 900 step mixed into the card rather than laid over it at an opacity,
451
+ so the surface is one solid colour on any ground. At full strength a 900
452
+ block outweighs everything around it, and step-300 text gains contrast on
453
+ the darker fill. */
454
+ --status-green-surface: color-mix(in oklab, var(--color-green-900) 40%, var(--card));
455
+ --status-amber-surface: color-mix(in oklab, var(--color-amber-900) 40%, var(--card));
456
+ --status-red-surface: color-mix(in oklab, var(--color-red-900) 40%, var(--card));
457
+ --status-blue-surface: color-mix(in oklab, var(--color-sky-900) 40%, var(--card));
409
458
  }
410
459
 
411
460
  @theme inline {
412
- --font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
461
+ --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
462
+ --font-sans--font-feature-settings: "cv11", "ss01";
413
463
  --font-mono: "Geist Mono Variable", ui-monospace, monospace;
414
-
415
- /* Typography scale — matches Spree admin (smaller than Tailwind defaults) */
464
+ /* Geist Mono's ligatures merge a space and a following `--` into one glyph,
465
+ hiding the space in command-line flags. */
466
+ --font-mono--font-feature-settings: "liga" 0, "calt" 0;
467
+
468
+ /* Inter at 13px reads thin under grayscale antialiasing, so every step
469
+ sits 50 heavier than the CSS keyword it replaces. */
470
+ --font-weight-normal: 450;
471
+ --font-weight-medium: 550;
472
+ --font-weight-semibold: 650;
473
+ /* Bold keeps a visible step above semibold, now that semibold is 650. */
474
+ --font-weight-bold: 750;
475
+
476
+ /* Typography scale — matches Spree admin (smaller than Tailwind defaults).
477
+ Each size carries its own line height on a 4px grid; left to Tailwind's,
478
+ they kept ratios tuned for its larger sizes and text of one size landed
479
+ on five different line heights. */
416
480
  --text-xs: 0.75rem;
481
+ --text-xs--line-height: 1rem;
482
+ /* 13px is the one text size; `text-base` is what components use, and
483
+ `text-sm` is kept equal so older or plugin markup lands on it too. */
417
484
  --text-sm: 0.8125rem;
418
- --text-base: 0.875rem;
485
+ --text-sm--line-height: 1.25rem;
486
+ --text-base: 0.8125rem;
487
+ --text-base--line-height: 1.25rem;
419
488
  --text-lg: 1rem;
489
+ --text-lg--line-height: 1.5rem;
420
490
  --text-xl: 1.25rem;
491
+ --text-xl--line-height: 1.75rem;
421
492
  --text-2xl: 1.5rem;
493
+ --text-2xl--line-height: 2rem;
422
494
  --text-3xl: 1.75rem;
495
+ --text-3xl--line-height: 2.25rem;
423
496
  --text-4xl: 2rem;
497
+ --text-4xl--line-height: 2.5rem;
424
498
 
425
499
  /* The resting lift on a control that sits ON a surface — a field, a button,
426
500
  a card. Measured off a reference: the ground under a field's bottom edge
@@ -430,7 +504,10 @@
430
504
  /* A form field's resting lift. Heavier than --shadow-xs and weighted to the
431
505
  bottom edge only, so the field reads as a slightly raised well without a
432
506
  tinted fill — the fill stays on the card colour (see --border-control). */
433
- --shadow-control: 0 0.5px 1px 0 rgb(0 0 0 / 0.05);
507
+ --shadow-control: var(--field-shadow);
508
+ /* Not `--shadow-card`: `--color-card` makes `shadow-card` a shadow colour. */
509
+ --shadow-card-lift: var(--card-shadow);
510
+ --shadow-field-focus: var(--field-focus-shadow);
434
511
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
435
512
  --shadow-nav: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.04) 0px 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 1px 2px -1px, rgba(0, 0, 0, 0.02) 0px 2px 4px 0px;
436
513
 
@@ -444,14 +521,15 @@
444
521
  /* Radius scale, remapped one step rounder than Tailwind's defaults.
445
522
  Corner radius is most of what reads as "soft", and remapping the scale
446
523
  reaches every `rounded-lg`/`rounded-xl` already written in the components
447
- — including the `calc(var(--radius-2xl) - 1px)` a card header derives its
524
+ — including the `calc(var(--radius-card) - 1px)` a card header derives its
448
525
  own corner from, which would drift out of alignment if the components
449
526
  were each edited by hand instead. */
450
527
  --radius-sm: 0.4rem;
451
528
  --radius-md: 0.5rem;
452
529
  --radius-lg: 0.625rem;
453
530
  --radius-xl: 0.75rem;
454
- --radius-2xl: 1rem;
531
+ --radius-2xl: 1.25rem; /* 20px: the shell sheet, dialogs, sheets and toasts */
532
+ --radius-card: 1rem; /* 16px: cards; what a card's inside edge derives from */
455
533
 
456
534
  /* Sidebar + header tokens */
457
535
  --spacing-sidebar-width: 240px;
@@ -497,8 +575,17 @@
497
575
  --color-input: var(--input);
498
576
  --color-border: var(--border);
499
577
  --color-border-card: var(--border-card);
578
+ --color-card-edge: var(--card-edge);
500
579
  --color-border-subtle: var(--border-subtle);
501
580
  --color-border-control: var(--border-control);
581
+ --color-field-border: var(--field-border);
582
+ --color-field-hover: var(--field-hover);
583
+ --color-switch-off: var(--switch-off);
584
+ --color-stepper: var(--stepper);
585
+ --color-stepper-hover: var(--stepper-hover);
586
+ --color-stepper-active: var(--stepper-active);
587
+ --color-sidebar-active: var(--sidebar-active);
588
+ --color-sidebar-active-hover: var(--sidebar-active-hover);
502
589
  --color-card-container: var(--card-container);
503
590
  --color-nested-raised: var(--nested-raised);
504
591
  --color-track-recessed: var(--track-recessed);
@@ -531,18 +618,22 @@
531
618
  --color-success: var(--status-green-fg);
532
619
  --color-success-bg: var(--status-green-bg);
533
620
  --color-success-border: var(--status-green-border);
621
+ --color-success-surface: var(--status-green-surface);
534
622
 
535
623
  --color-warning: var(--status-amber-fg);
536
624
  --color-warning-bg: var(--status-amber-bg);
537
625
  --color-warning-border: var(--status-amber-border);
626
+ --color-warning-surface: var(--status-amber-surface);
538
627
 
539
628
  --color-danger: var(--status-red-fg);
540
629
  --color-danger-bg: var(--status-red-bg);
541
630
  --color-danger-border: var(--status-red-border);
631
+ --color-danger-surface: var(--status-red-surface);
542
632
 
543
633
  --color-info: var(--status-blue-fg);
544
634
  --color-info-bg: var(--status-blue-bg);
545
635
  --color-info-border: var(--status-blue-border);
636
+ --color-info-surface: var(--status-blue-surface);
546
637
 
547
638
  /* Re-point Tailwind's blue ramp at the brand accent. Interactive blue is
548
639
  used throughout the component layer for focus borders, checked states and
@@ -698,12 +789,12 @@
698
789
 
699
790
  @layer base {
700
791
  :root {
701
- --scrollbar-thumb: oklch(86.9% 0.005 56.366); /* stone-300 */
702
- --scrollbar-thumb-hover: oklch(70.9% 0.01 56.259); /* stone-400 */
792
+ --scrollbar-thumb: oklch(87% 0 0); /* neutral-300 */
793
+ --scrollbar-thumb-hover: oklch(70.8% 0 0); /* neutral-400 */
703
794
  }
704
795
  .dark {
705
- --scrollbar-thumb: oklch(37.4% 0.01 67.558); /* stone-700 */
706
- --scrollbar-thumb-hover: oklch(44.4% 0.011 73.639); /* stone-600 */
796
+ --scrollbar-thumb: oklch(37.1% 0 0); /* neutral-700 */
797
+ --scrollbar-thumb-hover: oklch(43.9% 0 0); /* neutral-600 */
707
798
  }
708
799
 
709
800
  /* Themed scrollbar inside floating surfaces only — page scrollbar stays
@@ -764,7 +855,7 @@
764
855
  *
765
856
  * `pointer: coarse` rather than a width breakpoint: this is about the input
766
857
  * method, and a small window on a laptop should keep the compact type. */
767
- /* Nav rows and menu labels read small on a phone at the desktop 14px.
858
+ /* Nav rows and menu labels read small on a phone at the desktop 13px.
768
859
  * 16px is the size the platform uses for a primary list row. Sizing is left
769
860
  * to the components themselves — buttons carry their own touch targets. */
770
861
  @media (pointer: coarse) {
@@ -842,8 +933,7 @@
842
933
  }
843
934
 
844
935
  html {
845
- @apply font-sans antialiased;
846
- text-rendering: optimizeLegibility;
936
+ @apply font-sans font-normal antialiased;
847
937
  /* No `overflow-y: scroll` here. It used to reserve the scrollbar gutter so
848
938
  * the page would not jump sideways between a short route and a tall one,
849
939
  * back when the document was what scrolled. The inset shell ended that:
@@ -859,9 +949,11 @@
859
949
  max-width: 100vw;
860
950
  }
861
951
 
952
+ /* 13px, the one text size: components use `text-base` too, so text that
953
+ sets no size of its own lands on the same size as everything around it.
954
+ `text-xs` (12px) is the only smaller step, for captions and badges. */
862
955
  body {
863
956
  @apply bg-background text-foreground text-base;
864
- line-height: 1.5;
865
957
  }
866
958
 
867
959
  /* Headings — semibold (600) like Rails admin */
@@ -871,7 +963,7 @@
871
963
  h4,
872
964
  h5,
873
965
  h6 {
874
- @apply font-semibold leading-tight;
966
+ @apply font-semibold;
875
967
  }
876
968
 
877
969
  h1 {
@@ -895,6 +987,20 @@
895
987
  }
896
988
 
897
989
  @layer components {
990
+ /* A table that opens its card (the `Card > CardContent p-0 > Table` pattern)
991
+ rounds its header's top corners to the inside of the card's 1px border.
992
+ The header cells carry their own fill, so without this their square
993
+ corners stick out past the card's curve. Tables that manage their own
994
+ corners (`roundedTop`) set the same radius, so the two never disagree. */
995
+ [data-slot='card'] > [data-slot='card-content']:first-child > :first-child thead > tr:first-child {
996
+ & > th:first-child {
997
+ border-top-left-radius: calc(var(--radius-card) - 1px);
998
+ }
999
+ & > th:last-child {
1000
+ border-top-right-radius: calc(var(--radius-card) - 1px);
1001
+ }
1002
+ }
1003
+
898
1004
  /* Checkerboard for the color picker's "no color selected" swatch.
899
1005
  Two-tone diagonal pattern using `--border` + `--background` so it's
900
1006
  visible in both light and dark themes. */