@spree/dashboard-ui 1.0.0-beta.3 → 1.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/hooks/use-copy-to-clipboard.ts +6 -4
- package/src/spree/bulk-price-table.tsx +13 -0
- package/src/spree/data-grid/cells.tsx +9 -15
- package/src/spree/data-grid/data-grid.tsx +6 -2
- package/src/spree/icons.ts +3 -0
- package/src/spree/metadata/metadata-card.tsx +4 -1
- package/src/spree/products/status-card.tsx +6 -1
- package/src/styles.css +40 -8
- package/src/ui/checkbox.tsx +1 -1
- package/src/ui/combobox.tsx +2 -2
- package/src/ui/input-group.tsx +1 -1
- package/src/ui/input.tsx +1 -1
- package/src/ui/rich-text-editor.tsx +2 -2
- package/src/ui/select.tsx +1 -1
- package/src/ui/sidebar.tsx +57 -3
- package/src/ui/textarea.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-ui",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.5",
|
|
5
5
|
"description": "Spree Dashboard design system — shadcn primitives, headless composed components, and design tokens. Source-only package; consumers compile via their own Vite/Tailwind setup. Developer Preview.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tailwindcss": "^4.2.4"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@base-ui/react": "
|
|
50
|
+
"@base-ui/react": "1.8.0",
|
|
51
51
|
"@dnd-kit/core": "^6.3.1",
|
|
52
52
|
"@dnd-kit/sortable": "^10.0.0",
|
|
53
53
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"date-fns": "^4.1.0",
|
|
67
67
|
"date-fns-tz": "^3.2.0",
|
|
68
68
|
"flag-icons": "^7.5.0",
|
|
69
|
-
"lucide-react": "^1.
|
|
69
|
+
"lucide-react": "^1.47.0",
|
|
70
70
|
"react-colorful": "^5.7.0",
|
|
71
71
|
"react-day-picker": "^9.14.0",
|
|
72
72
|
"recharts": "^3.8.1",
|
|
@@ -7,8 +7,9 @@ interface UseCopyToClipboardOptions {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Wraps `navigator.clipboard.writeText` with a `copied` flag that flashes true
|
|
10
|
-
* for `resetMs` after a successful copy.
|
|
11
|
-
*
|
|
10
|
+
* for `resetMs` after a successful copy. `copy` resolves to whether it worked:
|
|
11
|
+
* it fails in insecure contexts or when permission is denied, and the flag
|
|
12
|
+
* then stays false.
|
|
12
13
|
*
|
|
13
14
|
* The reset timer is cleared on unmount so the component doesn't try to
|
|
14
15
|
* `setState` after it's gone.
|
|
@@ -24,7 +25,7 @@ export function useCopyToClipboard({ resetMs = 1200 }: UseCopyToClipboardOptions
|
|
|
24
25
|
}, [])
|
|
25
26
|
|
|
26
27
|
const copy = useCallback(
|
|
27
|
-
async (value: string) => {
|
|
28
|
+
async (value: string): Promise<boolean> => {
|
|
28
29
|
try {
|
|
29
30
|
await navigator.clipboard.writeText(value)
|
|
30
31
|
setCopied(true)
|
|
@@ -33,8 +34,9 @@ export function useCopyToClipboard({ resetMs = 1200 }: UseCopyToClipboardOptions
|
|
|
33
34
|
setCopied(false)
|
|
34
35
|
timerRef.current = null
|
|
35
36
|
}, resetMs)
|
|
37
|
+
return true
|
|
36
38
|
} catch {
|
|
37
|
-
|
|
39
|
+
return false
|
|
38
40
|
}
|
|
39
41
|
},
|
|
40
42
|
[resetMs],
|
|
@@ -92,6 +92,10 @@ export interface BulkPriceTableProps {
|
|
|
92
92
|
onTierAdd?: (rowId: string) => void
|
|
93
93
|
/** Removes a tier row. */
|
|
94
94
|
onTierRemove?: (rowId: string) => void
|
|
95
|
+
/** Why the last save was refused. Rendered above the grid rather than
|
|
96
|
+
* toasted: this table lives in a dialog, and the toast viewport sits
|
|
97
|
+
* below the overlay layer so nothing raised from in here is visible. */
|
|
98
|
+
error?: string | null
|
|
95
99
|
|
|
96
100
|
// Optional state for the toolbar + pagination footer. Omitting all of these
|
|
97
101
|
// hides the toolbar/pagination entirely (use for in-memory single-page lists).
|
|
@@ -125,6 +129,7 @@ export function BulkPriceTable({
|
|
|
125
129
|
totalPages,
|
|
126
130
|
onPageChange,
|
|
127
131
|
isLoading,
|
|
132
|
+
error,
|
|
128
133
|
}: BulkPriceTableProps) {
|
|
129
134
|
const columns = useMemo<ColumnDef<BulkPriceRow>[]>(
|
|
130
135
|
() => [
|
|
@@ -306,6 +311,14 @@ export function BulkPriceTable({
|
|
|
306
311
|
// grid between them should run to the container's edges, the way a
|
|
307
312
|
// table in a card does.
|
|
308
313
|
<div className="flex h-full flex-col gap-3">
|
|
314
|
+
{error && (
|
|
315
|
+
<div
|
|
316
|
+
role="alert"
|
|
317
|
+
className="mx-3 shrink-0 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive text-sm"
|
|
318
|
+
>
|
|
319
|
+
{error}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
309
322
|
{showToolbar && (
|
|
310
323
|
// Always-mounted toolbar. Conditionally rendering the search input
|
|
311
324
|
// would unmount it whenever a deferred query refetches into the
|
|
@@ -144,30 +144,26 @@ interface MoneyCellProps extends BaseCellProps {
|
|
|
144
144
|
/** Canonical decimal string (`12.50`) coming back from the API, or
|
|
145
145
|
* `null`/blank for unset. */
|
|
146
146
|
value: string | null
|
|
147
|
-
/** Receives the user's raw
|
|
148
|
-
*
|
|
149
|
-
*
|
|
147
|
+
/** Receives the user's raw, locale-formatted input (`"12,50"`). The caller
|
|
148
|
+
* normalizes it to canonical and passes that back as `value`, so the cell
|
|
149
|
+
* always shows what will be saved. `null` only when the user explicitly
|
|
150
|
+
* blanks the field. */
|
|
150
151
|
onChange: (next: string | null) => void
|
|
151
152
|
/** Currency symbol rendered as a non-editable prefix (`$`, `€`, `kr`, …).
|
|
152
153
|
* Omit for a plain decimal cell with no prefix. */
|
|
153
154
|
symbol?: string
|
|
154
155
|
/**
|
|
155
156
|
* Locale decimal separator used only for display (canonical `.` →
|
|
156
|
-
* locale char).
|
|
157
|
-
* backend handles thousands separators, grouped digits, and so on.
|
|
158
|
-
* Default `.`.
|
|
157
|
+
* locale char). Default `.`.
|
|
159
158
|
*/
|
|
160
159
|
decimal?: string
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
/**
|
|
164
163
|
* Editable money cell. Like `NumberCell` but keeps decimals (no truncation)
|
|
165
|
-
* and renders a currency-symbol prefix.
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* untouched on save. `Spree::LocalizedNumber.parse` on the backend handles
|
|
169
|
-
* locale-aware parsing (thousands separators, decimal comma, etc.) so the
|
|
170
|
-
* SPA doesn't have to reimplement it.
|
|
164
|
+
* and renders a currency-symbol prefix. Formats the canonical value with the
|
|
165
|
+
* locale's decimal mark for display and hands the user's raw input to
|
|
166
|
+
* `onChange` for the caller to normalize.
|
|
171
167
|
*/
|
|
172
168
|
export function MoneyCell({
|
|
173
169
|
coords,
|
|
@@ -219,9 +215,7 @@ export function MoneyCell({
|
|
|
219
215
|
})
|
|
220
216
|
|
|
221
217
|
function commit() {
|
|
222
|
-
//
|
|
223
|
-
// handles locale-aware parsing. Empty string is the only "clear"
|
|
224
|
-
// signal we generate frontend-side.
|
|
218
|
+
// Empty string is the only "clear" signal we generate frontend-side.
|
|
225
219
|
onChange(draft.trim() ? draft : null)
|
|
226
220
|
}
|
|
227
221
|
|
|
@@ -241,7 +241,11 @@ function DataGridShell<T>({
|
|
|
241
241
|
className,
|
|
242
242
|
)}
|
|
243
243
|
aria-label={ariaLabel}
|
|
244
|
-
|
|
244
|
+
// Bubble phase, never capture: React dispatches the two phases from
|
|
245
|
+
// separate native listeners and renders queued updates between
|
|
246
|
+
// them, so clearing `editing` here first would re-render the cell
|
|
247
|
+
// out of edit mode before its own blur commits the typed value.
|
|
248
|
+
onBlur={(e) => {
|
|
245
249
|
// A cell unmounting mid-commit is not the merchant leaving the
|
|
246
250
|
// grid: `relatedTarget` is null there, and the dialog's focus
|
|
247
251
|
// trap then claims the focus a frame later. Take it back on the
|
|
@@ -293,7 +297,7 @@ function DataGridShell<T>({
|
|
|
293
297
|
// The table-level classes zero this cell's own top and
|
|
294
298
|
// bottom borders: a border sits outside the padding box and
|
|
295
299
|
// the shadow inside, so leaving both draws two rules at rest.
|
|
296
|
-
className="h-8 bg-muted px-3 text-left font-medium shadow-[inset_0_-1px_0_0_var(--border)]"
|
|
300
|
+
className="h-8 bg-muted px-3 text-left font-medium text-nowrap shadow-[inset_0_-1px_0_0_var(--border)]"
|
|
297
301
|
>
|
|
298
302
|
{header.isPlaceholder
|
|
299
303
|
? null
|
package/src/spree/icons.ts
CHANGED
|
@@ -53,6 +53,7 @@ export {
|
|
|
53
53
|
FileArchiveIcon,
|
|
54
54
|
FileAudioIcon,
|
|
55
55
|
FileCodeIcon,
|
|
56
|
+
FileDownIcon,
|
|
56
57
|
FileIcon,
|
|
57
58
|
FileImageIcon,
|
|
58
59
|
FileSpreadsheetIcon,
|
|
@@ -135,10 +136,12 @@ export {
|
|
|
135
136
|
SlidersHorizontalIcon,
|
|
136
137
|
SparklesIcon,
|
|
137
138
|
SplitIcon,
|
|
139
|
+
SquareTextIcon,
|
|
138
140
|
StarIcon,
|
|
139
141
|
StoreIcon,
|
|
140
142
|
StrikethroughIcon,
|
|
141
143
|
SunIcon,
|
|
144
|
+
SwatchBookIcon,
|
|
142
145
|
TableIcon,
|
|
143
146
|
TagIcon,
|
|
144
147
|
TagsIcon,
|
|
@@ -22,7 +22,10 @@ export function MetadataCard({ metadata, title, emptyTitle, emptyDescription }:
|
|
|
22
22
|
return (
|
|
23
23
|
<Card>
|
|
24
24
|
<CardHeader>
|
|
25
|
-
<CardTitle>
|
|
25
|
+
<CardTitle>
|
|
26
|
+
<BracesIcon className="size-4" />
|
|
27
|
+
{title}
|
|
28
|
+
</CardTitle>
|
|
26
29
|
</CardHeader>
|
|
27
30
|
<CardContent>
|
|
28
31
|
{entries.length === 0 ? (
|
|
@@ -19,6 +19,7 @@ export function StatusCard<TFieldValues extends FieldValues>({
|
|
|
19
19
|
control,
|
|
20
20
|
name,
|
|
21
21
|
title,
|
|
22
|
+
icon,
|
|
22
23
|
label,
|
|
23
24
|
options,
|
|
24
25
|
disabled,
|
|
@@ -28,6 +29,7 @@ export function StatusCard<TFieldValues extends FieldValues>({
|
|
|
28
29
|
control: Control<TFieldValues>
|
|
29
30
|
name: Path<TFieldValues>
|
|
30
31
|
title: string
|
|
32
|
+
icon?: React.ReactNode
|
|
31
33
|
label: string
|
|
32
34
|
options: StatusOption[]
|
|
33
35
|
disabled?: boolean
|
|
@@ -42,7 +44,10 @@ export function StatusCard<TFieldValues extends FieldValues>({
|
|
|
42
44
|
return (
|
|
43
45
|
<Card>
|
|
44
46
|
<CardHeader>
|
|
45
|
-
<CardTitle>
|
|
47
|
+
<CardTitle>
|
|
48
|
+
{icon}
|
|
49
|
+
{title}
|
|
50
|
+
</CardTitle>
|
|
46
51
|
</CardHeader>
|
|
47
52
|
<CardContent className="flex flex-col gap-4">
|
|
48
53
|
<Field>
|
package/src/styles.css
CHANGED
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
sheet rather than a surface floating above it, so its edge only has to
|
|
154
154
|
suggest where the region ends. Used by `CardContent`'s frame; a dialog or
|
|
155
155
|
the sheet itself must keep --border, or the shell loses its outline. */
|
|
156
|
-
--border-card: color-mix(in srgb, var(--border-base)
|
|
156
|
+
--border-card: color-mix(in srgb, var(--border-base) 60%, transparent);
|
|
157
157
|
/* Dividers *inside* a container — card header/footer splits, dialog and
|
|
158
158
|
sheet separators, table row rules.
|
|
159
159
|
|
|
@@ -171,9 +171,9 @@
|
|
|
171
171
|
3.11:1 on the page ground, the lightest step that clears the minimum on
|
|
172
172
|
both.
|
|
173
173
|
|
|
174
|
-
No form control uses this. Fields, selects
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
No form control uses this. Fields, selects and comboboxes take `--border`
|
|
175
|
+
with `--shadow-control`; checkboxes and radios, too small to carry that
|
|
176
|
+
lift, keep `--shadow-xs` like the outline button.
|
|
177
177
|
|
|
178
178
|
A tinted fill on fields was tried and reverted. Disabled fields are
|
|
179
179
|
`--muted`, so any resting tint either collides with that meaning or (as
|
|
@@ -430,10 +430,12 @@
|
|
|
430
430
|
a card. Measured off a reference: the ground under a field's bottom edge
|
|
431
431
|
recovers over about 2px and nothing spreads above it or to the sides, so
|
|
432
432
|
this is purely directional, with no ambient layer. */
|
|
433
|
-
--shadow-xs: 0 1px
|
|
434
|
-
/*
|
|
435
|
-
|
|
436
|
-
|
|
433
|
+
--shadow-xs: 0 0px 1px 0 rgb(0 0 0 / 0.05);
|
|
434
|
+
/* A form field's resting lift. Heavier than --shadow-xs and weighted to the
|
|
435
|
+
bottom edge only, so the field reads as a slightly raised well without a
|
|
436
|
+
tinted fill — the fill stays on the card colour (see --border-control). */
|
|
437
|
+
--shadow-control: 0 0.5px 1px 0 rgb(0 0 0 / 0.05);
|
|
438
|
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
437
439
|
--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;
|
|
438
440
|
|
|
439
441
|
/* Floating surfaces — menus, popovers, dialogs, sheets. Wide and faint
|
|
@@ -576,6 +578,36 @@
|
|
|
576
578
|
}
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
@utility link {
|
|
582
|
+
/* The one treatment for an inline textual link — a link in prose, in help
|
|
583
|
+
text, inside an alert or an empty state. Colour rather than a permanent
|
|
584
|
+
underline, because the dashboard puts links inside dense body copy where
|
|
585
|
+
a rule under every one reads as noise; the underline arrives on hover so
|
|
586
|
+
the affordance is still there when the pointer looks for it.
|
|
587
|
+
|
|
588
|
+
NOT for links that are really something else: a table row, a breadcrumb,
|
|
589
|
+
a nav item, a card wrapped in an anchor, a button rendered as `<a>`.
|
|
590
|
+
Those carry their own treatment and `.link` would fight it.
|
|
591
|
+
|
|
592
|
+
`focus-visible` draws a ring rather than borrowing `focus-ring`'s border
|
|
593
|
+
+shadow: a text link has no border to recolour, so it needs an outline
|
|
594
|
+
that follows the text box instead. */
|
|
595
|
+
color: var(--link);
|
|
596
|
+
text-decoration-line: none;
|
|
597
|
+
text-underline-offset: 3px;
|
|
598
|
+
border-radius: var(--radius-sm);
|
|
599
|
+
|
|
600
|
+
&:hover {
|
|
601
|
+
color: var(--link-hover);
|
|
602
|
+
text-decoration-line: underline;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
&:focus-visible {
|
|
606
|
+
outline: 2px solid var(--ring);
|
|
607
|
+
outline-offset: 2px;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
579
611
|
@utility themed-scrollbar {
|
|
580
612
|
/* Theme-aware thin scrollbar utility. Track is transparent, thumb uses
|
|
581
613
|
CSS vars defined in :root and .dark, so it adapts to the active theme.
|
package/src/ui/checkbox.tsx
CHANGED
|
@@ -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 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-border shadow-xs 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
|
package/src/ui/combobox.tsx
CHANGED
|
@@ -57,7 +57,7 @@ function ComboboxButtonTrigger({
|
|
|
57
57
|
data-slot="combobox-button-trigger"
|
|
58
58
|
data-size={size}
|
|
59
59
|
className={cn(
|
|
60
|
-
"group/combobox-trigger flex w-full min-h-8 cursor-pointer items-center justify-between gap-1.5 rounded-md border border-border 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-
|
|
60
|
+
"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",
|
|
61
61
|
className,
|
|
62
62
|
)}
|
|
63
63
|
{...props}
|
|
@@ -295,7 +295,7 @@ function ComboboxChips({
|
|
|
295
295
|
// resize lets the positioner follow the anchor smoothly; making it
|
|
296
296
|
// instant jumps the open popup out from under the pointer, so the next
|
|
297
297
|
// option click lands on nothing.
|
|
298
|
-
'flex flex-wrap items-center gap-1.5 rounded-md border border-border 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',
|
|
298
|
+
'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',
|
|
299
299
|
className,
|
|
300
300
|
)}
|
|
301
301
|
{...props}
|
package/src/ui/input-group.tsx
CHANGED
|
@@ -11,7 +11,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
11
11
|
data-slot="input-group"
|
|
12
12
|
role="group"
|
|
13
13
|
className={cn(
|
|
14
|
-
'group/input-group relative flex w-full min-w-0 items-center rounded-md border border-border bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:cursor-not-allowed has-disabled:text-muted-foreground has-[[data-slot=input-group-control]:focus-visible]:border-blue-500 has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
|
|
14
|
+
'group/input-group relative flex w-full min-w-0 items-center rounded-md border border-border shadow-control bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:cursor-not-allowed has-disabled:text-muted-foreground has-[[data-slot=input-group-control]:focus-visible]:border-blue-500 has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)] has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
|
|
15
15
|
className,
|
|
16
16
|
)}
|
|
17
17
|
{...props}
|
package/src/ui/input.tsx
CHANGED
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
|
8
8
|
type={type}
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
|
-
'block w-full min-w-0 min-h-8 rounded-md border border-border bg-card py-1.5 px-2.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none placeholder:text-muted-foreground 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 file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
11
|
+
'block w-full min-w-0 min-h-8 rounded-md border border-border shadow-control bg-card py-1.5 px-2.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none placeholder:text-muted-foreground 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 file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
12
12
|
className,
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
@@ -191,7 +191,7 @@ export function RichTextEditor({
|
|
|
191
191
|
// in a form beside them without reading as a different kind of control.
|
|
192
192
|
// `focus-within` rather than `focus`: the element that takes the caret
|
|
193
193
|
// is Tiptap's inner contenteditable, not this wrapper.
|
|
194
|
-
'rounded-md border border-border bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out focus-within:border-blue-500 focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]',
|
|
194
|
+
'rounded-md border border-border shadow-control bg-card text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out focus-within:border-blue-500 focus-within:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]',
|
|
195
195
|
disabled && 'pointer-events-none bg-muted text-muted-foreground shadow-none',
|
|
196
196
|
className,
|
|
197
197
|
)}
|
|
@@ -252,7 +252,7 @@ function EditorToolbar({
|
|
|
252
252
|
return (
|
|
253
253
|
<div
|
|
254
254
|
data-slot="rich-text-editor-toolbar"
|
|
255
|
-
className="flex items-center gap-0.5 border-b border-border px-2 py-1.5"
|
|
255
|
+
className="flex items-center gap-0.5 border-b border-border-subtle px-2 py-1.5"
|
|
256
256
|
>
|
|
257
257
|
<ToolbarButton
|
|
258
258
|
active={toolbar.isBold}
|
package/src/ui/select.tsx
CHANGED
|
@@ -66,7 +66,7 @@ function SelectTrigger({
|
|
|
66
66
|
// A row mixing a select with a popover trigger (the dashboard's channel
|
|
67
67
|
// picker beside its date range) then reads as one set of controls
|
|
68
68
|
// rather than two. The default size is a form field and stays flat.
|
|
69
|
-
"group/select-trigger flex w-full min-h-8 cursor-pointer items-center justify-between gap-1.5 rounded-md border border-border 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-[placeholder]:text-muted-foreground data-[size=sm]:shadow-
|
|
69
|
+
"group/select-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-[placeholder]:text-muted-foreground 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 data-[size=sm]:[&_svg:not([class*='size-'])]:size-3.5 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
70
70
|
className,
|
|
71
71
|
)}
|
|
72
72
|
{...props}
|
package/src/ui/sidebar.tsx
CHANGED
|
@@ -421,13 +421,67 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S
|
|
|
421
421
|
)
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Width of `element`'s vertical scrollbar in pixels — 0 while it does not
|
|
426
|
+
* overflow, and 0 wherever the platform overlays its scrollbars. A scrollbar
|
|
427
|
+
* appearing shrinks the content box, so observing the size catches it.
|
|
428
|
+
*/
|
|
429
|
+
function useScrollbarWidth(element: HTMLElement | null) {
|
|
430
|
+
const [width, setWidth] = React.useState(0)
|
|
431
|
+
|
|
432
|
+
React.useLayoutEffect(() => {
|
|
433
|
+
if (!element) return
|
|
434
|
+
const measure = () => setWidth(element.offsetWidth - element.clientWidth)
|
|
435
|
+
const observer = new ResizeObserver(measure)
|
|
436
|
+
observer.observe(element)
|
|
437
|
+
measure()
|
|
438
|
+
return () => observer.disconnect()
|
|
439
|
+
}, [element])
|
|
440
|
+
|
|
441
|
+
return width
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function SidebarContent({ className, style, ref, ...props }: React.ComponentProps<'div'>) {
|
|
445
|
+
const [element, setElement] = React.useState<HTMLDivElement | null>(null)
|
|
446
|
+
const scrollbarWidth = useScrollbarWidth(element)
|
|
447
|
+
|
|
448
|
+
const setRefs = React.useCallback(
|
|
449
|
+
(node: HTMLDivElement | null) => {
|
|
450
|
+
setElement(node)
|
|
451
|
+
if (typeof ref === 'function') ref(node)
|
|
452
|
+
else if (ref) ref.current = node
|
|
453
|
+
},
|
|
454
|
+
[ref],
|
|
455
|
+
)
|
|
456
|
+
|
|
425
457
|
return (
|
|
426
458
|
<div
|
|
459
|
+
ref={setRefs}
|
|
427
460
|
data-slot="sidebar-content"
|
|
428
461
|
data-sidebar="content"
|
|
462
|
+
style={
|
|
463
|
+
{ '--sidebar-scrollbar-width': `${scrollbarWidth}px`, ...style } as React.CSSProperties
|
|
464
|
+
}
|
|
429
465
|
className={cn(
|
|
430
|
-
|
|
466
|
+
// `scroll-fade` dissolves the rows into the header above and the pinned
|
|
467
|
+
// entries below as they scroll past. Its defaults suit a long page, not
|
|
468
|
+
// a rail: at most 40px of fade, easing out across the last 96px of
|
|
469
|
+
// scroll, which left a row sliced flat against Settings whenever the
|
|
470
|
+
// list was nearly at its end. A row-deep fade that holds until the
|
|
471
|
+
// final 1rem reads as a fade at every position; `pb-3` lets the last
|
|
472
|
+
// row come clear of it at the end.
|
|
473
|
+
//
|
|
474
|
+
// The icon rail scrolls too, with no scrollbar: a rail too narrow for
|
|
475
|
+
// one would otherwise let its icons run over the pinned entries and the
|
|
476
|
+
// account row. Its tooltips and hover menus are portalled, so nothing
|
|
477
|
+
// needs to escape the box.
|
|
478
|
+
'quiet-scrollbar scroll-fade [--scroll-fade-size:3rem] [--scroll-fade-reveal:1rem] flex min-h-0 flex-1 flex-col gap-0 pt-3 pb-3 overflow-auto group-data-[collapsible=icon]:no-scrollbar',
|
|
479
|
+
// A scrollbar would otherwise take its width out of the rows, leaving
|
|
480
|
+
// them short of the header and the pinned entries. It moves into the
|
|
481
|
+
// sidebar's own padding instead (up to 0.5rem), and anything wider
|
|
482
|
+
// comes out of the groups' end padding — so rows end in the same place
|
|
483
|
+
// whether the list scrolls or not.
|
|
484
|
+
'-me-[min(var(--sidebar-scrollbar-width),0.5rem)] **:data-[slot=sidebar-group]:pe-[calc(0.5rem_-_max(0px,var(--sidebar-scrollbar-width)_-_0.5rem))] group-data-[collapsible=icon]:**:data-[slot=sidebar-group]:pe-0',
|
|
431
485
|
className,
|
|
432
486
|
)}
|
|
433
487
|
{...props}
|
|
@@ -654,7 +708,7 @@ function SidebarMenuBadge({ className, ...props }: React.ComponentProps<'div'>)
|
|
|
654
708
|
data-slot="sidebar-menu-badge"
|
|
655
709
|
data-sidebar="menu-badge"
|
|
656
710
|
className={cn(
|
|
657
|
-
'pointer-events-none flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md border border-sidebar-border px-1.5 text-[11px] font-medium text-sidebar-foreground/70 tabular-nums select-none',
|
|
711
|
+
'pointer-events-none flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md border border-sidebar-border bg-sidebar px-1.5 text-[11px] font-medium text-sidebar-foreground/70 tabular-nums select-none',
|
|
658
712
|
'transition-colors duration-100 ease-out group-data-[collapsible=icon]:hidden',
|
|
659
713
|
// Follows its own row: brighter under the pointer, full strength when
|
|
660
714
|
// the item is the active one.
|
package/src/ui/textarea.tsx
CHANGED
|
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
|
7
7
|
<textarea
|
|
8
8
|
data-slot="textarea"
|
|
9
9
|
className={cn(
|
|
10
|
-
'flex field-sizing-content min-h-16 w-full rounded-md border border-border bg-card px-2.5 py-1.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none placeholder:text-muted-foreground 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',
|
|
10
|
+
'flex field-sizing-content min-h-16 w-full rounded-md border border-border shadow-control bg-card px-2.5 py-1.5 text-base font-normal leading-normal text-foreground transition-[color,background-color,border-color,box-shadow] duration-100 ease-out outline-none placeholder:text-muted-foreground 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',
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|