@spree/dashboard-ui 0.13.1 → 1.0.0-beta.1
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/README.md +1 -1
- package/package.json +15 -9
- package/src/hooks/use-prefers-reduced-motion.ts +26 -0
- package/src/hooks/use-scrolled.ts +34 -14
- package/src/index.ts +39 -1
- package/src/lib/date-range-presets.ts +79 -0
- package/src/lib/same-rich-text.ts +145 -0
- package/src/spree/address-block.tsx +5 -4
- package/src/spree/address-book-row.tsx +96 -0
- package/src/spree/address-map.tsx +62 -0
- package/src/spree/back-button.tsx +14 -9
- package/src/spree/bulk-dialog.tsx +2 -2
- package/src/spree/bulk-price-table.tsx +145 -14
- package/src/spree/bulk-variants-table.tsx +434 -0
- package/src/spree/calculator-summary.tsx +5 -1
- package/src/spree/claim-resolve-dialog.tsx +219 -0
- package/src/spree/confirm-dialog.tsx +1 -1
- package/src/spree/copy-to-clipboard-button.tsx +1 -1
- package/src/spree/data-grid/cells.tsx +199 -25
- package/src/spree/data-grid/context.ts +6 -0
- package/src/spree/data-grid/data-grid.tsx +149 -7
- package/src/spree/data-grid/helpers.ts +6 -2
- package/src/spree/data-grid/index.ts +9 -1
- package/src/spree/data-grid/use-data-grid-keyboard.ts +4 -2
- package/src/spree/delivery-form-dialog.tsx +166 -0
- package/src/spree/drag-handle.tsx +2 -2
- package/src/spree/editable-note-card.tsx +103 -0
- package/src/spree/external-link.tsx +56 -0
- package/src/spree/file-type-icon.tsx +68 -0
- package/src/spree/form-actions.tsx +2 -3
- package/src/spree/form-section.tsx +46 -0
- package/src/spree/fulfill-items-form.tsx +282 -0
- package/src/spree/fulfillment-edit-dialog.tsx +211 -0
- package/src/spree/fulfillment-item-list.tsx +61 -0
- package/src/spree/fulfillment-panel.tsx +53 -0
- package/src/spree/icons.ts +167 -0
- package/src/spree/json-preview-drawer.tsx +4 -4
- package/src/spree/label-upload-dialog.tsx +195 -0
- package/src/spree/language-menu-items.tsx +37 -30
- package/src/spree/metadata/metadata-card.tsx +1 -1
- package/src/spree/order-cancel-dialog.tsx +149 -0
- package/src/spree/post-sale-create-dialogs.tsx +253 -0
- package/src/spree/post-sale-fields.tsx +133 -0
- package/src/spree/product-membership-list.tsx +453 -0
- package/src/spree/products/status-card.tsx +79 -0
- package/src/spree/quantity-tier-editor.tsx +162 -0
- package/src/spree/resource-combobox.tsx +12 -1
- package/src/spree/resource-name-cell.tsx +21 -4
- package/src/spree/return-dialogs.tsx +238 -0
- package/src/spree/route-error-boundary.tsx +1 -1
- package/src/spree/row-actions.tsx +2 -2
- package/src/spree/search-input.tsx +76 -0
- package/src/spree/secret-input.tsx +26 -40
- package/src/spree/tax-identifiers-card.tsx +418 -0
- package/src/spree/theme-toggle.tsx +36 -23
- package/src/spree/wizard.tsx +254 -0
- package/src/styles.css +750 -75
- package/src/ui/accordion.tsx +83 -0
- package/src/ui/alert.tsx +79 -0
- package/src/ui/attachment.tsx +1 -1
- package/src/ui/badge.tsx +198 -56
- package/src/ui/breadcrumb.tsx +1 -1
- package/src/ui/button.tsx +22 -10
- package/src/ui/calendar.tsx +1 -1
- package/src/ui/card.tsx +52 -6
- package/src/ui/checkbox.tsx +13 -5
- package/src/ui/combobox.tsx +125 -6
- package/src/ui/command.tsx +149 -23
- package/src/ui/context-menu.tsx +99 -0
- package/src/ui/data-table.tsx +448 -9
- package/src/ui/date-picker.tsx +18 -3
- package/src/ui/date-range-picker.tsx +47 -60
- package/src/ui/dialog.tsx +6 -6
- package/src/ui/dropdown-menu.tsx +18 -11
- package/src/ui/field.tsx +10 -2
- package/src/ui/input-group.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/map.tsx +2110 -0
- package/src/ui/media-preview.tsx +105 -0
- package/src/ui/pagination.tsx +19 -11
- package/src/ui/popover.tsx +4 -2
- package/src/ui/progress.tsx +5 -2
- package/src/ui/radio-group.tsx +4 -4
- package/src/ui/rich-text-editor.tsx +214 -88
- package/src/ui/scroll-area.tsx +65 -0
- package/src/ui/select.tsx +17 -10
- package/src/ui/separator.tsx +1 -1
- package/src/ui/sheet.tsx +49 -7
- package/src/ui/sidebar.tsx +138 -37
- package/src/ui/skeleton.tsx +3 -1
- package/src/ui/tabs.tsx +84 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/thumbnail.tsx +80 -0
- package/src/ui/toast.tsx +112 -0
- package/src/ui/tooltip.tsx +3 -1
- package/src/ui/sonner.tsx +0 -47
package/src/styles.css
CHANGED
|
@@ -29,78 +29,387 @@
|
|
|
29
29
|
Adapted from spree/admin Tailwind design system to match look & feel.
|
|
30
30
|
============================================================================ */
|
|
31
31
|
|
|
32
|
+
/* Brand palette.
|
|
33
|
+
*
|
|
34
|
+
* The two themes take different neutral families, for the reason each one's
|
|
35
|
+
* large areas need.
|
|
36
|
+
*
|
|
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.
|
|
53
|
+
*
|
|
54
|
+
* Primary CTAs stay near-black rather than adopting the site's blue button:
|
|
55
|
+
* in a dense admin UI the blue has to stay meaningful as a selection and
|
|
56
|
+
* focus signal, which it stops being once every page renders a saturated
|
|
57
|
+
* blue button. */
|
|
32
58
|
:root {
|
|
33
|
-
/*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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`.
|
|
66
|
+
|
|
67
|
+
Tokens that must equal another token point at it with var() rather than
|
|
68
|
+
repeating its value, so the two cannot drift. */
|
|
69
|
+
--brand-blue: oklch(0.5967 0.2212 258); /* #0077ff — site accent */
|
|
70
|
+
--brand-blue-strong: oklch(0.5106 0.1868 257.7); /* #0060ce — site link hover */
|
|
71
|
+
/* Links in body copy. The plain accent measures 4.13:1 on white and 4.21:1
|
|
72
|
+
on the dark card — under 4.5 in BOTH themes — so a link takes its own
|
|
73
|
+
step rather than the accent. */
|
|
74
|
+
--link: oklch(0.5106 0.1868 257.7);
|
|
75
|
+
--link-hover: oklch(0.44 0.16 257.7);
|
|
76
|
+
|
|
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 */
|
|
79
|
+
--card: oklch(1 0 0); /* #ffffff — cards and inputs read as raised */
|
|
80
|
+
--card-foreground: var(--foreground);
|
|
81
|
+
--popover: var(--card);
|
|
82
|
+
--popover-foreground: var(--foreground);
|
|
83
|
+
--primary: var(--foreground); /* stone-950 — near-black primary CTAs */
|
|
84
|
+
--primary-foreground: var(--card);
|
|
85
|
+
/* Filled surfaces (table headers, badges, secondary buttons) read as a
|
|
86
|
+
whisper against the white card rather than a distinct band. */
|
|
87
|
+
--secondary: oklch(97% 0.001 106.424); /* stone-100 */
|
|
88
|
+
--secondary-foreground: var(--foreground);
|
|
89
|
+
--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
|
|
96
|
+
--secondary: a hover and a filled surface are the same weight of "one step
|
|
97
|
+
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.
|
|
99
|
+
|
|
100
|
+
It is also the lightest step that keeps --muted-foreground legible on it:
|
|
101
|
+
the previous bespoke grey measured 4.47:1, just under the 4.5 this file
|
|
102
|
+
claims everywhere else. The sidebar shares it via --sidebar-accent. */
|
|
103
|
+
--accent: var(--secondary);
|
|
104
|
+
--accent-foreground: var(--foreground);
|
|
105
|
+
/* The *selected* state, and any control that must read as raised while
|
|
106
|
+
sitting on an already-accented surface: the sidebar's active nav item, a
|
|
107
|
+
ghost button hovered inside a hovered table row.
|
|
108
|
+
|
|
109
|
+
--accent cannot do this job. It is the hover/fill surface, so it has to
|
|
110
|
+
stay light enough for --muted-foreground to clear 4.5:1 on it — which
|
|
111
|
+
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.
|
|
114
|
+
|
|
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
|
|
118
|
+
the two tokens into one and leave a hovered item darker than a selected
|
|
119
|
+
one. This sits between them. */
|
|
120
|
+
--accent-strong: oklch(94.7% 0.002 56);
|
|
121
|
+
/* Hover, between --accent and --accent-strong so a nav item has somewhere to
|
|
122
|
+
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.
|
|
125
|
+
|
|
126
|
+
--accent is too light to be a hover here: on the sidebar's ground (the
|
|
127
|
+
page colour, not a card) it measures 1.04:1, which is invisible. */
|
|
128
|
+
--accent-hover: oklch(95.9% 0.002 56);
|
|
129
|
+
/* The active item hovered. It darkens rather than lightening: a selected
|
|
130
|
+
row that goes paler under the pointer reads as being deselected. Stays one
|
|
131
|
+
step under --accent-strong, or the two land on the same value and a
|
|
132
|
+
selected item stops responding to the pointer. */
|
|
133
|
+
--accent-strong-hover: oklch(92.3% 0.003 48.717); /* stone-200 */
|
|
48
134
|
--destructive: oklch(0.58 0.22 27); /* red-600 */
|
|
49
|
-
--destructive-foreground:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
--
|
|
135
|
+
--destructive-foreground: var(--card);
|
|
136
|
+
/* The full-strength rule colour. Nothing styles with it directly — the two
|
|
137
|
+
tokens below are both a share of it, so neither compounds the other's
|
|
138
|
+
reduction.
|
|
139
|
+
|
|
140
|
+
stone-200. This is the floor for a hairline: anything lighter stops
|
|
141
|
+
holding a container's edge on a white sheet, and the row rules of a long
|
|
142
|
+
table start to disappear before the frame around them does. */
|
|
143
|
+
--border-base: oklch(92.3% 0.003 48.717); /* stone-200 */
|
|
144
|
+
/* A structural edge — the content sheet, a dialog, a dropdown, a popover, a
|
|
145
|
+
table's frame. These are surfaces that float above the page or divide it,
|
|
146
|
+
and their edge is what says where one ends and the next begins, so it
|
|
147
|
+
carries the base at full strength. Cards take --border-card instead.
|
|
148
|
+
|
|
149
|
+
Carries its strength in the token, so call sites never restate it — an
|
|
150
|
+
`/80` utility would compound on top of this. */
|
|
151
|
+
--border: var(--border-base);
|
|
152
|
+
/* A card's own edge. Softer than a structural one: a card is a region OF the
|
|
153
|
+
sheet rather than a surface floating above it, so its edge only has to
|
|
154
|
+
suggest where the region ends. Used by `CardContent`'s frame; a dialog or
|
|
155
|
+
the sheet itself must keep --border, or the shell loses its outline. */
|
|
156
|
+
--border-card: color-mix(in srgb, var(--border-base) 50%, transparent);
|
|
157
|
+
/* Dividers *inside* a container — card header/footer splits, dialog and
|
|
158
|
+
sheet separators, table row rules.
|
|
159
|
+
|
|
160
|
+
Half of the BASE, not of --border, so it is a true 50% rather than one
|
|
161
|
+
token compounding the other. The same value as --border-card, so a card's
|
|
162
|
+
edge and the rules dividing its contents are one hairline weight.
|
|
163
|
+
|
|
164
|
+
Resolved to a real colour rather than an opacity utility so it also works
|
|
165
|
+
inside arbitrary values such as the sticky table header's inset shadow,
|
|
166
|
+
where `/50` has no effect. */
|
|
167
|
+
--border-subtle: color-mix(in srgb, var(--border-base) 50%, transparent);
|
|
168
|
+
/* A mark that has to hold its own against the surface with nothing else
|
|
169
|
+
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.
|
|
173
|
+
|
|
174
|
+
No form control uses this. Fields, selects, comboboxes, checkboxes and
|
|
175
|
+
radios all take `--border` with `--shadow-xs`, the same pair the outline
|
|
176
|
+
button wears, so every bordered control on a card reads as one family.
|
|
177
|
+
|
|
178
|
+
A tinted fill on fields was tried and reverted. Disabled fields are
|
|
179
|
+
`--muted`, so any resting tint either collides with that meaning or (as
|
|
180
|
+
happened) lands darker than it and inverts the signal outright. On this
|
|
181
|
+
palette the enabled state has to stay on the card colour. */
|
|
182
|
+
--border-control: oklch(86.9% 0.005 56.366);
|
|
183
|
+
/* A card that *holds* other records — an order's fulfillments, its returns,
|
|
184
|
+
its claims — rather than holding content directly.
|
|
185
|
+
|
|
186
|
+
It takes the page's own colour, so the card contributes a border and a
|
|
187
|
+
title strip and nothing else: the white records inside are the only thing
|
|
188
|
+
with a surface, and they carry all the weight. Points at --background
|
|
189
|
+
rather than repeating its value, so the two cannot drift apart.
|
|
190
|
+
|
|
191
|
+
Giving the container a fill of its own was tried and reads worse — it
|
|
192
|
+
tints a large area to say something the border already says, and the
|
|
193
|
+
records lose contrast against it. Sitting at the page colour is safe
|
|
194
|
+
because the card still draws its 1px border at 1.21:1; the fill was never
|
|
195
|
+
carrying that boundary.
|
|
196
|
+
|
|
197
|
+
Records inside stay on `--card`, so what a merchant reads is the raised,
|
|
198
|
+
white thing. Pushing them into a grey well instead reads as content that
|
|
199
|
+
has been demoted, which is backwards — the fulfillment is the point of
|
|
200
|
+
the card, not its chrome. */
|
|
201
|
+
--card-container: var(--background);
|
|
202
|
+
/* Light mode's raised record is simply the card surface. */
|
|
203
|
+
--nested-raised: var(--card);
|
|
204
|
+
/* The recessed well a raised thing sits in — a tab strip's track, a
|
|
205
|
+
segmented control. Paired with --nested-raised: the track always sits one
|
|
206
|
+
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. */
|
|
208
|
+
--track-recessed: var(--accent);
|
|
209
|
+
--input: color-mix(in srgb, var(--border-base) 80%, transparent);
|
|
210
|
+
--ring: var(--brand-blue);
|
|
211
|
+
--chart-1: oklch(0.7663 0.1067 254.4);
|
|
212
|
+
--chart-2: oklch(0.5967 0.2212 258); /* brand blue */
|
|
213
|
+
--chart-3: oklch(0.5106 0.1868 257.7);
|
|
214
|
+
--chart-4: oklch(0.4232 0.1524 257.4);
|
|
215
|
+
--chart-5: oklch(0.3358 0.118 257.1);
|
|
58
216
|
--radius: 0.75rem;
|
|
59
217
|
|
|
60
|
-
/* Sidebar —
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
--sidebar
|
|
66
|
-
--sidebar-
|
|
67
|
-
--sidebar-
|
|
68
|
-
--sidebar-
|
|
218
|
+
/* Sidebar — the ground the content sheet is inset into, so it sits one step
|
|
219
|
+
back from the page rather than level with it. That difference is what
|
|
220
|
+
makes the sheet's rounded edge and shadow read at all: when the two were
|
|
221
|
+
the same colour the inset was invisible. Everything else still points at
|
|
222
|
+
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);
|
|
225
|
+
--sidebar-primary: var(--primary);
|
|
226
|
+
--sidebar-primary-foreground: var(--primary-foreground);
|
|
227
|
+
--sidebar-accent: var(--accent);
|
|
228
|
+
--sidebar-accent-foreground: var(--accent-foreground);
|
|
229
|
+
--sidebar-accent-strong: var(--accent-strong);
|
|
230
|
+
--sidebar-accent-hover: var(--accent-strong);
|
|
231
|
+
--sidebar-accent-strong-hover: var(--accent-strong-hover);
|
|
232
|
+
/* Matches the rule the top bar draws, so every edge framing the shell —
|
|
233
|
+
the sidebar's, the top bar's, the settings header's — is one weight. */
|
|
234
|
+
--sidebar-border: color-mix(in srgb, var(--border) 75%, transparent);
|
|
235
|
+
--sidebar-ring: var(--ring);
|
|
236
|
+
|
|
237
|
+
/* Status ramps, following Vercel's Geist colour system.
|
|
238
|
+
*
|
|
239
|
+
* Geist gives each step of a ramp a fixed job: low steps are component
|
|
240
|
+
* backgrounds, mid steps borders, high steps text and icons. Its badges pair
|
|
241
|
+
* a light tint with a dark reading colour from the same hue, which is what
|
|
242
|
+
* these trios reproduce — a fill light enough to sit on a white card, under
|
|
243
|
+
* text that is a true text colour rather than the fill darkened.
|
|
244
|
+
*
|
|
245
|
+
* The values come from Tailwind's own palette rather than Geist's raw
|
|
246
|
+
* numbers: each step below is the closest match in oklch to its Geist
|
|
247
|
+
* counterpart, so the result reads the same while staying on the palette the
|
|
248
|
+
* rest of the dashboard is built from.
|
|
249
|
+
*
|
|
250
|
+
* Text lands on step 700 in every ramp, over a step-100 fill. Green is the
|
|
251
|
+
* one exception: Tailwind's green-700 is lighter than its siblings and lands
|
|
252
|
+
* at 4.497:1 on green-100, a hair under the 4.5:1 that 12px badge text
|
|
253
|
+
* needs, so green's fill drops one step to clear it. The difference is
|
|
254
|
+
* invisible beside the other three but keeps the whole set measurably legible
|
|
255
|
+
* rather than passing only by eye. */
|
|
256
|
+
--status-green-bg: var(--color-green-100);
|
|
257
|
+
--status-green-border: var(--color-green-200);
|
|
258
|
+
--status-green-fg: var(--color-green-700);
|
|
259
|
+
|
|
260
|
+
--status-amber-bg: var(--color-amber-100);
|
|
261
|
+
--status-amber-border: var(--color-amber-200);
|
|
262
|
+
--status-amber-fg: var(--color-amber-700);
|
|
263
|
+
|
|
264
|
+
--status-red-bg: var(--color-red-100);
|
|
265
|
+
--status-red-border: var(--color-red-200);
|
|
266
|
+
--status-red-fg: var(--color-red-700);
|
|
267
|
+
|
|
268
|
+
/* Blue reads off the sky ramp rather than Tailwind's blue: `--color-blue-500`
|
|
269
|
+
and `-600` are remapped to the brand accent further down, so a badge built
|
|
270
|
+
on those steps would read as a brand mark rather than a neutral status. */
|
|
271
|
+
--status-blue-bg: var(--color-sky-100);
|
|
272
|
+
--status-blue-border: var(--color-sky-200);
|
|
273
|
+
--status-blue-fg: var(--color-sky-700);
|
|
69
274
|
}
|
|
70
275
|
|
|
71
276
|
.dark {
|
|
72
|
-
/* Vercel-style "true black" base, with surfaces stepping up in lightness.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
277
|
+
/* Vercel-style "true black" base, with surfaces stepping up in lightness.
|
|
278
|
+
*
|
|
279
|
+
* Every neutral below is zero-chroma — a true grey, not light mode's warm
|
|
280
|
+
* stone. See the palette note at the top of this file for why the two
|
|
281
|
+
* themes split. The lightness ladder is unchanged from the warm version;
|
|
282
|
+
* only the tint was removed.
|
|
283
|
+
*
|
|
284
|
+
* Dark surfaces step in roughly 4-5 points of OKLCH lightness, not the 1-2
|
|
285
|
+
* light mode uses. Near-black compresses perceived difference, so a step
|
|
286
|
+
* that reads clearly on white disappears down here: an earlier version of
|
|
287
|
+
* this ramp had sidebar, muted, background and card inside a 3.5-point band
|
|
288
|
+
* and the whole middle of the screen read as one flat surface, which in turn
|
|
289
|
+
* made the one boundary that was visible — the sidebar's — look like the
|
|
290
|
+
* sheet had been lit. Reference dark dashboards space adjacent surfaces
|
|
291
|
+
* 5-6 points apart for the same reason.
|
|
292
|
+
*
|
|
293
|
+
* The ladder, ground upward: sidebar 14 / muted + background 17 / card 21.6
|
|
294
|
+
* / secondary + nested-raised 25 / accent 29.5 / accent-hover 33 /
|
|
295
|
+
* accent-strong 38. --card is the anchor, since the inset sheet is the
|
|
296
|
+
* surface a merchant actually reads; keep the gaps if you move anything. */
|
|
297
|
+
--background: oklch(17% 0 0);
|
|
298
|
+
--foreground: oklch(95% 0 0);
|
|
299
|
+
--card: oklch(21.6% 0 0);
|
|
300
|
+
--card-foreground: var(--foreground);
|
|
301
|
+
--popover: var(--card);
|
|
302
|
+
--popover-foreground: var(--foreground);
|
|
303
|
+
/* Mirrors the light theme's primary: there a navy fill carries white text,
|
|
304
|
+
so here the two swap rather than the button taking on its own colour. */
|
|
305
|
+
--primary: var(--foreground);
|
|
306
|
+
--primary-foreground: oklch(14.7% 0 0); /* light theme's primary */
|
|
307
|
+
/* Above --card, but only just. These fill elements that sit *on* a card
|
|
308
|
+
(combobox chips, badges, secondary buttons, table header rows), so a value
|
|
309
|
+
at or under the card's lightness erases them — while too big a step reads
|
|
310
|
+
as a bright band across a full-width table header. Dark mode raises where
|
|
311
|
+
light mode lowers, since a surface reads as raised by getting lighter. */
|
|
312
|
+
--secondary: oklch(25% 0 0);
|
|
313
|
+
--secondary-foreground: var(--foreground);
|
|
314
|
+
--muted: oklch(17% 0 0);
|
|
315
|
+
/* At stone-400's lightness. The old value failed 4.5:1 on --accent
|
|
316
|
+
(4.20:1); this clears every dark ground, the accent included. */
|
|
317
|
+
--muted-foreground: oklch(70.9% 0 0);
|
|
318
|
+
--accent: oklch(29.5% 0 0);
|
|
319
|
+
--accent-foreground: var(--foreground);
|
|
320
|
+
--accent-strong: oklch(38% 0 0);
|
|
321
|
+
--accent-hover: oklch(33% 0 0);
|
|
322
|
+
--accent-strong-hover: oklch(44.4% 0 0);
|
|
87
323
|
--destructive: oklch(0.704 0.191 22.216);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
324
|
+
/* A white veil over whatever surface it crosses, so one value works on the
|
|
325
|
+
page ground and the raised sheet alike. Both shares below are half of it,
|
|
326
|
+
as in light mode, so a container's edge and the rules inside it are one
|
|
327
|
+
hairline weight. */
|
|
328
|
+
--border-base: oklch(1 0 0 / 16%);
|
|
329
|
+
--border: var(--border-base);
|
|
330
|
+
--border-card: color-mix(in srgb, var(--border-base) 50%, transparent);
|
|
331
|
+
--border-subtle: color-mix(in srgb, var(--border-base) 50%, transparent);
|
|
332
|
+
/* Light mode darkens the control edge away from the card; dark mode lightens
|
|
333
|
+
it. Not the light value inverted — measured on this theme's own surfaces:
|
|
334
|
+
3.22:1 on the card, 3.34:1 on the page ground. */
|
|
335
|
+
--border-control: oklch(52% 0 0);
|
|
336
|
+
/* Same arrangement as light — container at the page colour, records raised
|
|
337
|
+
above it. 1.20:1 between them here. Records take `--nested-raised` rather
|
|
338
|
+
than `--card`: the card token is a step above the page, which is where the
|
|
339
|
+
container would sit if it had a fill of its own. */
|
|
340
|
+
--card-container: var(--background);
|
|
341
|
+
--nested-raised: oklch(25% 0 0);
|
|
342
|
+
/* Not --accent here: dark mode's accent is lighter than every raised
|
|
343
|
+
surface, so a shared token would put the selected tab below its own
|
|
344
|
+
track. --muted is the step under --nested-raised. */
|
|
345
|
+
--track-recessed: var(--muted);
|
|
346
|
+
--input: color-mix(in srgb, var(--border-base) 80%, transparent);
|
|
347
|
+
/* The same blue as light mode, lifted for a dark ground. The previous value
|
|
348
|
+
was a near-grey (chroma 0.016) despite claiming parity, so a focused field
|
|
349
|
+
in dark mode drew a grey halo and lost the one signal that says "this is
|
|
350
|
+
where typing goes". */
|
|
351
|
+
--ring: oklch(0.68 0.19 258);
|
|
352
|
+
--link: oklch(0.7 0.16 258);
|
|
353
|
+
--link-hover: oklch(0.78 0.14 258);
|
|
91
354
|
--chart-1: oklch(0.809 0.105 251.813);
|
|
92
355
|
--chart-2: oklch(0.623 0.214 259.815);
|
|
93
356
|
--chart-3: oklch(0.546 0.245 262.881);
|
|
94
357
|
--chart-4: oklch(0.488 0.243 264.376);
|
|
95
358
|
--chart-5: oklch(0.424 0.199 265.638);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
359
|
+
/* One step back from the page, as in light mode — see the note there. Darker
|
|
360
|
+
rather than lighter: the sheet is the raised surface, so the ground behind
|
|
361
|
+
it recedes.
|
|
362
|
+
|
|
363
|
+
Measured against --card, not --background: the shell renders the sidebar's
|
|
364
|
+
`inset` variant, so the sheet beside it is the card surface and that
|
|
365
|
+
pairing is what a merchant actually sees.
|
|
366
|
+
|
|
367
|
+
The sheet looking "too bright" was never this token being too dark — it
|
|
368
|
+
was the four surfaces above it sitting within half a point of each other,
|
|
369
|
+
so the sidebar edge was the only visible boundary on the screen and it
|
|
370
|
+
carried the whole contrast budget alone. Widening those steps (see the
|
|
371
|
+
ramp note on --background) fixes it; this stays low so the ground still
|
|
372
|
+
recedes. */
|
|
373
|
+
--sidebar: oklch(14% 0 0);
|
|
374
|
+
--sidebar-foreground: var(--foreground);
|
|
375
|
+
--sidebar-primary: var(--primary);
|
|
376
|
+
--sidebar-primary-foreground: var(--primary-foreground);
|
|
377
|
+
--sidebar-accent: var(--accent);
|
|
378
|
+
--sidebar-accent-foreground: var(--accent-foreground);
|
|
379
|
+
--sidebar-accent-strong: var(--accent-strong);
|
|
380
|
+
--sidebar-accent-hover: var(--accent-hover);
|
|
381
|
+
--sidebar-accent-strong-hover: var(--accent-strong-hover);
|
|
382
|
+
/* Matches the rule the top bar draws, so every edge framing the shell —
|
|
383
|
+
the sidebar's, the top bar's, the settings header's — is one weight. */
|
|
384
|
+
--sidebar-border: color-mix(in srgb, var(--border) 75%, transparent);
|
|
385
|
+
--sidebar-ring: var(--ring);
|
|
386
|
+
|
|
387
|
+
/* Geist's dark ramps are not the light ones dimmed — the fill becomes a deep
|
|
388
|
+
shade of the hue and the text becomes brighter than its light-mode
|
|
389
|
+
counterpart. That inversion is why a tint expressed as an opacity over
|
|
390
|
+
black goes muddy: these are separate colours, not one hue at lower alpha.
|
|
391
|
+
Border and fill share a step here, since a deep fill already separates
|
|
392
|
+
from a black card and a lighter rule around it would draw more attention
|
|
393
|
+
than the badge itself.
|
|
394
|
+
|
|
395
|
+
Text sits on step 300 across all four ramps — the brightest of the steps
|
|
396
|
+
Geist treats as text, and the only one that clears 4.5:1 on a 900 fill for
|
|
397
|
+
every hue (red and sky fall under it by step 400). */
|
|
398
|
+
--status-green-bg: var(--color-green-900);
|
|
399
|
+
--status-green-border: var(--color-green-900);
|
|
400
|
+
--status-green-fg: var(--color-green-300);
|
|
401
|
+
|
|
402
|
+
--status-amber-bg: var(--color-amber-900);
|
|
403
|
+
--status-amber-border: var(--color-amber-900);
|
|
404
|
+
--status-amber-fg: var(--color-amber-300);
|
|
405
|
+
|
|
406
|
+
--status-red-bg: var(--color-red-900);
|
|
407
|
+
--status-red-border: var(--color-red-900);
|
|
408
|
+
--status-red-fg: var(--color-red-300);
|
|
409
|
+
|
|
410
|
+
--status-blue-bg: var(--color-sky-900);
|
|
411
|
+
--status-blue-border: var(--color-sky-900);
|
|
412
|
+
--status-blue-fg: var(--color-sky-300);
|
|
104
413
|
}
|
|
105
414
|
|
|
106
415
|
@theme inline {
|
|
@@ -117,19 +426,67 @@
|
|
|
117
426
|
--text-3xl: 1.75rem;
|
|
118
427
|
--text-4xl: 2rem;
|
|
119
428
|
|
|
120
|
-
/*
|
|
121
|
-
|
|
122
|
-
|
|
429
|
+
/* The resting lift on a control that sits ON a surface — a field, a button,
|
|
430
|
+
a card. Measured off a reference: the ground under a field's bottom edge
|
|
431
|
+
recovers over about 2px and nothing spreads above it or to the sides, so
|
|
432
|
+
this is purely directional, with no ambient layer. */
|
|
433
|
+
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
434
|
+
/* The same lift, under its Tailwind-conventional name. Points at the token
|
|
435
|
+
above rather than repeating the value, so the two cannot drift. */
|
|
436
|
+
--shadow-sm: var(--shadow-xs);
|
|
123
437
|
--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;
|
|
124
438
|
|
|
439
|
+
/* Floating surfaces — menus, popovers, dialogs, sheets. Wide and faint
|
|
440
|
+
rather than tight and dark: the job is to say "this is above the page",
|
|
441
|
+
which a large soft spread does without drawing an edge of its own. A
|
|
442
|
+
hairline ring carries the actual boundary. */
|
|
443
|
+
--shadow-md: 0 4px 16px -4px rgb(0 0 0 / 0.08), 0 1px 4px -2px rgb(0 0 0 / 0.05);
|
|
444
|
+
--shadow-lg: 0 8px 32px -8px rgb(0 0 0 / 0.10), 0 2px 8px -4px rgb(0 0 0 / 0.06);
|
|
445
|
+
|
|
446
|
+
/* Radius scale, remapped one step rounder than Tailwind's defaults.
|
|
447
|
+
Corner radius is most of what reads as "soft", and remapping the scale
|
|
448
|
+
reaches every `rounded-lg`/`rounded-xl` already written in the components
|
|
449
|
+
— including the `calc(var(--radius-2xl) - 1px)` a card header derives its
|
|
450
|
+
own corner from, which would drift out of alignment if the components
|
|
451
|
+
were each edited by hand instead. */
|
|
452
|
+
--radius-sm: 0.4rem;
|
|
453
|
+
--radius-md: 0.5rem;
|
|
454
|
+
--radius-lg: 0.625rem;
|
|
455
|
+
--radius-xl: 0.875rem;
|
|
456
|
+
--radius-2xl: 1.125rem;
|
|
457
|
+
|
|
125
458
|
/* Sidebar + header tokens */
|
|
126
|
-
--spacing-sidebar-width:
|
|
127
|
-
|
|
459
|
+
--spacing-sidebar-width: 240px;
|
|
460
|
+
/* The offset pinned bands (a table's column row, the bulk-action bar) sit
|
|
461
|
+
below. There is no top bar, so the only chrome above the scrolling content
|
|
462
|
+
is a page's own `PageHeader` — which measures itself and overwrites this
|
|
463
|
+
at runtime. `0px` is therefore the honest default: it is what a page with
|
|
464
|
+
no header should use, and a non-zero constant here would strand the column
|
|
465
|
+
row that far down on every one of them. */
|
|
466
|
+
--spacing-header-height: 0px;
|
|
467
|
+
/* The height of a rail header row — the store/seller switcher, the settings
|
|
468
|
+
rail's title. A fixed constant, deliberately NOT the sticky offset above:
|
|
469
|
+
that one is measured and can legitimately be zero, which would collapse
|
|
470
|
+
these rows to nothing. */
|
|
471
|
+
--spacing-rail-header-height: 42px;
|
|
472
|
+
|
|
473
|
+
/* Easing. Overrides Tailwind's built-in `ease-out`, so every `ease-out`
|
|
474
|
+
utility in the app picks this up: the stock curve is too weak to read as
|
|
475
|
+
deliberate on anything larger than a colour change. `ease-out` is the
|
|
476
|
+
right default for UI — it starts fast, so the moment the user is watching
|
|
477
|
+
happens immediately. Movement across the screen wants `ease-in-out`
|
|
478
|
+
instead; `ease-in` is wrong for UI at any size, since it delays that
|
|
479
|
+
same moment. */
|
|
480
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
|
|
481
|
+
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
|
|
128
482
|
|
|
129
483
|
/* Color mappings */
|
|
130
484
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
131
485
|
--color-sidebar-border: var(--sidebar-border);
|
|
132
486
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
487
|
+
--color-sidebar-accent-strong: var(--sidebar-accent-strong);
|
|
488
|
+
--color-sidebar-accent-hover: var(--sidebar-accent-hover);
|
|
489
|
+
--color-sidebar-accent-strong-hover: var(--sidebar-accent-strong-hover);
|
|
133
490
|
--color-sidebar-accent: var(--sidebar-accent);
|
|
134
491
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
135
492
|
--color-sidebar-primary: var(--sidebar-primary);
|
|
@@ -143,9 +500,18 @@
|
|
|
143
500
|
--color-ring: var(--ring);
|
|
144
501
|
--color-input: var(--input);
|
|
145
502
|
--color-border: var(--border);
|
|
503
|
+
--color-border-card: var(--border-card);
|
|
504
|
+
--color-border-subtle: var(--border-subtle);
|
|
505
|
+
--color-border-control: var(--border-control);
|
|
506
|
+
--color-card-container: var(--card-container);
|
|
507
|
+
--color-nested-raised: var(--nested-raised);
|
|
508
|
+
--color-track-recessed: var(--track-recessed);
|
|
146
509
|
--color-destructive: var(--destructive);
|
|
147
510
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
148
511
|
--color-accent-foreground: var(--accent-foreground);
|
|
512
|
+
--color-accent-strong: var(--accent-strong);
|
|
513
|
+
--color-accent-hover: var(--accent-hover);
|
|
514
|
+
--color-accent-strong-hover: var(--accent-strong-hover);
|
|
149
515
|
--color-accent: var(--accent);
|
|
150
516
|
--color-muted-foreground: var(--muted-foreground);
|
|
151
517
|
--color-muted: var(--muted);
|
|
@@ -160,18 +526,49 @@
|
|
|
160
526
|
--color-foreground: var(--foreground);
|
|
161
527
|
--color-background: var(--background);
|
|
162
528
|
|
|
163
|
-
/*
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
529
|
+
/* Status colours — badges, alerts and any other success/warning/error mark.
|
|
530
|
+
Each is a background/border/foreground trio rather than a single hue, so a
|
|
531
|
+
component states which role it wants instead of picking an opacity and
|
|
532
|
+
hoping it lands. This replaces the older single-value `--color-success`
|
|
533
|
+
family, which had to be re-tinted at every call site and was bypassed by
|
|
534
|
+
most of them for exactly that reason. */
|
|
535
|
+
--color-success: var(--status-green-fg);
|
|
536
|
+
--color-success-bg: var(--status-green-bg);
|
|
537
|
+
--color-success-border: var(--status-green-border);
|
|
538
|
+
|
|
539
|
+
--color-warning: var(--status-amber-fg);
|
|
540
|
+
--color-warning-bg: var(--status-amber-bg);
|
|
541
|
+
--color-warning-border: var(--status-amber-border);
|
|
542
|
+
|
|
543
|
+
--color-danger: var(--status-red-fg);
|
|
544
|
+
--color-danger-bg: var(--status-red-bg);
|
|
545
|
+
--color-danger-border: var(--status-red-border);
|
|
546
|
+
|
|
547
|
+
--color-info: var(--status-blue-fg);
|
|
548
|
+
--color-info-bg: var(--status-blue-bg);
|
|
549
|
+
--color-info-border: var(--status-blue-border);
|
|
550
|
+
|
|
551
|
+
/* Re-point Tailwind's blue ramp at the brand accent. Interactive blue is
|
|
552
|
+
used throughout the component layer for focus borders, checked states and
|
|
553
|
+
selected calendar days (`border-blue-500`, `bg-blue-500/15`, …); mapping
|
|
554
|
+
the palette entry keeps every one of those on the site's accent without
|
|
555
|
+
each component having to know the token name. Only the interactive steps
|
|
556
|
+
are overridden — the rest of the ramp keeps Tailwind's values for
|
|
557
|
+
incidental use such as charts and illustrations. */
|
|
558
|
+
--color-blue-500: var(--brand-blue);
|
|
559
|
+
--color-blue-600: var(--brand-blue-strong);
|
|
560
|
+
--color-link: var(--link);
|
|
561
|
+
--color-link-hover: var(--link-hover);
|
|
168
562
|
}
|
|
169
563
|
|
|
170
564
|
@utility focus-ring {
|
|
171
565
|
/* Unified focus treatment — matches the form-input focus (soft blue glow,
|
|
172
566
|
no hard outline). Apply to buttons, links, menu triggers, and any other
|
|
173
567
|
interactive control so every focusable element looks the same on
|
|
174
|
-
keyboard focus.
|
|
568
|
+
keyboard focus. Text fields spell the same treatment out inline with a
|
|
569
|
+
`focus:` variant rather than `focus-visible:` — clicking into a field puts
|
|
570
|
+
the caret there, so it must show it owns the keyboard even when the focus
|
|
571
|
+
came from a pointer. */
|
|
175
572
|
outline: none;
|
|
176
573
|
&:focus-visible {
|
|
177
574
|
border-color: var(--ring);
|
|
@@ -209,14 +606,78 @@
|
|
|
209
606
|
}
|
|
210
607
|
}
|
|
211
608
|
|
|
609
|
+
@utility quiet-scrollbar {
|
|
610
|
+
/* Like `themed-scrollbar`, but the thumb stays invisible until the element
|
|
611
|
+
is hovered. For navigation panels and other chrome, where a permanently
|
|
612
|
+
visible track draws a heavy stripe down an otherwise calm surface. The
|
|
613
|
+
gutter is reserved either way, so revealing the thumb never reflows the
|
|
614
|
+
content. */
|
|
615
|
+
/* `scrollbar-color` is not animatable, so the Firefox path snaps between
|
|
616
|
+
states; the webkit thumb below fades. Both are acceptable — the reveal is
|
|
617
|
+
incidental, not something the user is watching for. */
|
|
618
|
+
scrollbar-color: transparent transparent;
|
|
619
|
+
scrollbar-width: thin;
|
|
620
|
+
|
|
621
|
+
&:hover,
|
|
622
|
+
&:focus-within {
|
|
623
|
+
scrollbar-color: var(--scrollbar-thumb) transparent;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
&::-webkit-scrollbar {
|
|
627
|
+
width: 10px;
|
|
628
|
+
height: 10px;
|
|
629
|
+
}
|
|
630
|
+
&::-webkit-scrollbar-track {
|
|
631
|
+
background: transparent;
|
|
632
|
+
}
|
|
633
|
+
&::-webkit-scrollbar-thumb {
|
|
634
|
+
background-color: transparent;
|
|
635
|
+
border: 2px solid transparent;
|
|
636
|
+
border-radius: 9999px;
|
|
637
|
+
background-clip: padding-box;
|
|
638
|
+
transition: background-color 150ms ease-out;
|
|
639
|
+
}
|
|
640
|
+
&:hover::-webkit-scrollbar-thumb,
|
|
641
|
+
&:focus-within::-webkit-scrollbar-thumb {
|
|
642
|
+
background-color: var(--scrollbar-thumb);
|
|
643
|
+
}
|
|
644
|
+
&::-webkit-scrollbar-thumb:hover {
|
|
645
|
+
background-color: var(--scrollbar-thumb-hover);
|
|
646
|
+
}
|
|
647
|
+
&::-webkit-scrollbar-corner {
|
|
648
|
+
background: transparent;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* A bare `divide-y`/`divide-x` is a rule *inside* a container — a list's row
|
|
653
|
+
splits, a stacked set of fields — which is exactly what --border-subtle is
|
|
654
|
+
for.
|
|
655
|
+
|
|
656
|
+
Tailwind v4's divide utilities set only the border WIDTH on the children;
|
|
657
|
+
their colour comes from `border-color`, which the universal `*` rule in the
|
|
658
|
+
base layer sets to --border. This has to live in the `utilities` layer to
|
|
659
|
+
outrank that — layer order decides before specificity does — and has to
|
|
660
|
+
match the same children the utilities do.
|
|
661
|
+
|
|
662
|
+
The WHOLE selector goes inside `:where()`, not just the parent: `:not()`
|
|
663
|
+
carries the specificity of its argument, so leaving `:not(:last-child)`
|
|
664
|
+
outside gave this rule a pseudo-class's weight and let it beat an explicit
|
|
665
|
+
`divide-border` at the call site — the opposite of the intent. Inside
|
|
666
|
+
`:where()` the rule has zero specificity and any named colour wins. */
|
|
667
|
+
@layer utilities {
|
|
668
|
+
:where(.divide-y > :not(:last-child), .divide-x > :not(:last-child)) {
|
|
669
|
+
border-color: var(--border-subtle);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
212
673
|
@layer base {
|
|
213
674
|
:root {
|
|
214
|
-
--scrollbar-thumb: oklch(
|
|
215
|
-
--scrollbar-thumb-hover: oklch(
|
|
675
|
+
--scrollbar-thumb: oklch(86.9% 0.005 56.366); /* stone-300 */
|
|
676
|
+
--scrollbar-thumb-hover: oklch(70.9% 0.01 56.259); /* stone-400 */
|
|
216
677
|
}
|
|
217
678
|
.dark {
|
|
218
|
-
--scrollbar-thumb: oklch(
|
|
219
|
-
--scrollbar-thumb-hover: oklch(
|
|
679
|
+
--scrollbar-thumb: oklch(37.4% 0.01 67.558); /* stone-700 */
|
|
680
|
+
--scrollbar-thumb-hover: oklch(44.4% 0.011 73.639); /* stone-600 */
|
|
220
681
|
}
|
|
221
682
|
|
|
222
683
|
/* Themed scrollbar inside floating surfaces only — page scrollbar stays
|
|
@@ -270,10 +731,99 @@
|
|
|
270
731
|
outline: none;
|
|
271
732
|
}
|
|
272
733
|
|
|
734
|
+
/* iOS Safari zooms the viewport when a focused text field's font is under
|
|
735
|
+
* 16px, and this app's `text-base` is 14px — so tapping any input on an
|
|
736
|
+
* iPhone jumped the page and left it zoomed. Raise the rendered size on
|
|
737
|
+
* coarse pointers only; the desktop density is unchanged.
|
|
738
|
+
*
|
|
739
|
+
* `pointer: coarse` rather than a width breakpoint: this is about the input
|
|
740
|
+
* method, and a small window on a laptop should keep the compact type. */
|
|
741
|
+
/* Nav rows and menu labels read small on a phone at the desktop 14px.
|
|
742
|
+
* 16px is the size the platform uses for a primary list row. Sizing is left
|
|
743
|
+
* to the components themselves — buttons carry their own touch targets. */
|
|
744
|
+
@media (pointer: coarse) {
|
|
745
|
+
[data-slot='sidebar-menu-button'],
|
|
746
|
+
[data-slot='sidebar-menu-sub-button'],
|
|
747
|
+
[data-slot='dropdown-menu-item'],
|
|
748
|
+
[data-slot='select-item'],
|
|
749
|
+
[data-slot='command-item'],
|
|
750
|
+
[data-slot='filter-panel-item'] {
|
|
751
|
+
font-size: 1rem !important;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Menu and listbox rows are sized for a pointer — roughly 29px, against a
|
|
756
|
+
* 44pt touch minimum — so on a phone they are hard to hit and easy to hit
|
|
757
|
+
* wrongly. Raise them on coarse pointers only; the dense desktop menus are
|
|
758
|
+
* unchanged. Same `!important` reasoning as the font-size rule below: these
|
|
759
|
+
* rows carry utility classes that outrank any selector written here. */
|
|
760
|
+
@media (pointer: coarse) {
|
|
761
|
+
[data-slot='dropdown-menu-item'],
|
|
762
|
+
[data-slot='dropdown-menu-checkbox-item'],
|
|
763
|
+
[data-slot='dropdown-menu-radio-item'],
|
|
764
|
+
[data-slot='dropdown-menu-sub-trigger'],
|
|
765
|
+
[data-slot='select-item'],
|
|
766
|
+
[data-slot='command-item'],
|
|
767
|
+
[data-slot='combobox-item'],
|
|
768
|
+
/* The filter panel's own rows: fields, values and date presets. They are
|
|
769
|
+
hand-rolled buttons rather than menu items, so they carry this slot
|
|
770
|
+
deliberately to opt into the same touch sizing. */
|
|
771
|
+
[data-slot='filter-panel-item'] {
|
|
772
|
+
min-height: 2.75rem !important;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* The rules above size what is *inside* an open menu; these size the
|
|
776
|
+
* control that opens it. A row's "⋮" is 24px, the sidebar toggle 28px and
|
|
777
|
+
* a table's sort/columns buttons 34px — each clears the 24px web minimum
|
|
778
|
+
* but sits well under what a finger wants, and the row action is both the
|
|
779
|
+
* smallest and the most repeated target on a list.
|
|
780
|
+
*
|
|
781
|
+
* Restricted to `button` because these slots wrap arbitrary children:
|
|
782
|
+
* `tooltip-trigger` in particular renders whatever it is given, including
|
|
783
|
+
* plain text, which must not be inflated to 44px. Width as well as height,
|
|
784
|
+
* since these are square icon buttons rather than full-width rows. */
|
|
785
|
+
button[data-slot='dropdown-menu-trigger'],
|
|
786
|
+
button[data-slot='sidebar-trigger'],
|
|
787
|
+
button[data-slot='popover-trigger'],
|
|
788
|
+
button[data-slot='tooltip-trigger'] {
|
|
789
|
+
min-height: 2.75rem !important;
|
|
790
|
+
min-width: 2.75rem !important;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* The controls inside the filter panel's value step — the operator select,
|
|
794
|
+
its input and the buttons under them. They are sized for a pointer
|
|
795
|
+
(`size="sm"`, roughly 30px), which is below the touch minimum on the one
|
|
796
|
+
surface where a merchant is most likely to be on a phone. */
|
|
797
|
+
[data-slot='filter-panel-controls'] [data-slot='select-trigger'],
|
|
798
|
+
[data-slot='filter-panel-controls'] [data-slot='input'],
|
|
799
|
+
[data-slot='filter-panel-controls'] button {
|
|
800
|
+
min-height: 2.75rem !important;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
@media (pointer: coarse) {
|
|
805
|
+
/* `!important` is load-bearing here, not laziness: these controls carry a
|
|
806
|
+
* `text-*` utility class, which outranks any element selector this rule
|
|
807
|
+
* could write. Without it the declaration loses to `.text-base` and the
|
|
808
|
+
* zoom still happens. The rule is deliberately narrow — one property, on
|
|
809
|
+
* text-entry controls, under a coarse pointer. */
|
|
810
|
+
input:not([type='checkbox']):not([type='radio']),
|
|
811
|
+
textarea,
|
|
812
|
+
select,
|
|
813
|
+
[contenteditable] {
|
|
814
|
+
font-size: max(1rem, var(--text-base)) !important;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
273
818
|
html {
|
|
274
819
|
@apply font-sans antialiased;
|
|
275
820
|
text-rendering: optimizeLegibility;
|
|
276
|
-
overflow-y: scroll
|
|
821
|
+
/* No `overflow-y: scroll` here. It used to reserve the scrollbar gutter so
|
|
822
|
+
* the page would not jump sideways between a short route and a tall one,
|
|
823
|
+
* back when the document was what scrolled. The inset shell ended that:
|
|
824
|
+
* it is exactly viewport-height and scrolls inside itself, so the document
|
|
825
|
+
* can never overflow — and the reserved gutter became a permanently
|
|
826
|
+
* disabled second scrollbar sitting beside the sheet's real one. */
|
|
277
827
|
/* Clip horizontal overflow at the document root so wide tables on mobile
|
|
278
828
|
* can't widen the page. Must live on <html>, not <body>: `overflow-x:
|
|
279
829
|
* hidden` on body makes `overflow-y` compute to `auto`, turning body into
|
|
@@ -335,4 +885,129 @@
|
|
|
335
885
|
5px -5px,
|
|
336
886
|
-5px 0;
|
|
337
887
|
}
|
|
888
|
+
|
|
889
|
+
/* Tiptap editor surface. Tailwind preflight strips list markers, link
|
|
890
|
+
underlines and block margins, so lists, quotes and links would save
|
|
891
|
+
correctly but look like plain paragraphs until the storefront rendered
|
|
892
|
+
them. These rules restore that chrome only inside the editor. */
|
|
893
|
+
.tiptap {
|
|
894
|
+
min-height: 8rem;
|
|
895
|
+
outline: none;
|
|
896
|
+
|
|
897
|
+
& p.is-editor-empty:first-child::before {
|
|
898
|
+
color: var(--muted-foreground);
|
|
899
|
+
content: attr(data-placeholder);
|
|
900
|
+
float: left;
|
|
901
|
+
height: 0;
|
|
902
|
+
pointer-events: none;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
& p {
|
|
906
|
+
margin-block: 0.5em;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
& p:first-child {
|
|
910
|
+
margin-block-start: 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
& p:last-child {
|
|
914
|
+
margin-block-end: 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
& ul {
|
|
918
|
+
list-style-type: disc;
|
|
919
|
+
margin-block: 0.5em;
|
|
920
|
+
padding-inline-start: 1.5em;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
& ol {
|
|
924
|
+
list-style-type: decimal;
|
|
925
|
+
margin-block: 0.5em;
|
|
926
|
+
padding-inline-start: 1.5em;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
& ul ul,
|
|
930
|
+
& ol ul {
|
|
931
|
+
list-style-type: circle;
|
|
932
|
+
margin-block: 0.15em;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
& ul ul ul,
|
|
936
|
+
& ol ul ul,
|
|
937
|
+
& ol ol ul,
|
|
938
|
+
& ul ol ul {
|
|
939
|
+
list-style-type: square;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
& ol ol,
|
|
943
|
+
& ul ol {
|
|
944
|
+
list-style-type: lower-alpha;
|
|
945
|
+
margin-block: 0.15em;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
& li {
|
|
949
|
+
margin-block: 0.15em;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
& li > p {
|
|
953
|
+
margin-block: 0;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
& blockquote {
|
|
957
|
+
border-inline-start: 3px solid var(--border);
|
|
958
|
+
color: var(--muted-foreground);
|
|
959
|
+
margin-block: 0.75em;
|
|
960
|
+
padding-inline-start: 1em;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
& a {
|
|
964
|
+
color: var(--brand-blue);
|
|
965
|
+
text-decoration: underline;
|
|
966
|
+
text-underline-offset: 2px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
& img {
|
|
970
|
+
max-width: 100%;
|
|
971
|
+
border-radius: var(--radius-md);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
& img.ProseMirror-selectednode {
|
|
975
|
+
outline: 2px solid var(--brand-blue);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/* Reduced motion. Deliberately NOT inside `@layer base`: unlayered CSS always
|
|
981
|
+
outranks layered CSS, so a base-layer rule loses to every `transition-*`
|
|
982
|
+
utility and the preference would be silently ignored. Unlayered and last in
|
|
983
|
+
the file, this wins on source order instead.
|
|
984
|
+
|
|
985
|
+
Movement is what the setting asks us to drop, so `transform`, `translate`
|
|
986
|
+
and `scale` are absent from the property list below — no element can animate
|
|
987
|
+
position or size however it is styled. Colour, shadow and opacity keep their
|
|
988
|
+
normal timing: they change appearance in place rather than moving anything,
|
|
989
|
+
and killing them too reads as broken rather than calm.
|
|
990
|
+
|
|
991
|
+
Components needing more than this — where suppressing a transition leaves a
|
|
992
|
+
jump rather than removing a movement — read the preference directly via
|
|
993
|
+
`usePrefersReducedMotion` and hold position, or opt out with a
|
|
994
|
+
`motion-reduce:` utility. */
|
|
995
|
+
@media (prefers-reduced-motion: reduce) {
|
|
996
|
+
*,
|
|
997
|
+
*::before,
|
|
998
|
+
*::after {
|
|
999
|
+
transition-property: color, background-color, border-color, box-shadow, opacity, fill, stroke;
|
|
1000
|
+
transition-duration: 100ms;
|
|
1001
|
+
/* Holds looping decorative animations at their first frame. */
|
|
1002
|
+
animation-duration: 0.01ms;
|
|
1003
|
+
animation-iteration-count: 1;
|
|
1004
|
+
scroll-behavior: auto;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/* Spinners are the deliberate exception: they signal that work is ongoing
|
|
1008
|
+
and have no static equivalent. */
|
|
1009
|
+
.animate-spin {
|
|
1010
|
+
animation-duration: 1s;
|
|
1011
|
+
animation-iteration-count: infinite;
|
|
1012
|
+
}
|
|
338
1013
|
}
|