@tristan2828/ui-foundation 4.62.0 → 4.63.0
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/conventions/docs/design-language.md +132 -23
- package/dist/components/app/app-shell.js +28 -2
- package/dist/components/app/board.js +1 -1
- package/dist/components/app/data-table.js +1 -1
- package/dist/components/app/foundation-providers.d.ts +14 -1
- package/dist/components/app/foundation-providers.js +4 -3
- package/dist/components/app/module-card.js +1 -1
- package/dist/components/app/palette-provider.d.ts +14 -0
- package/dist/components/app/palette-provider.js +35 -0
- package/dist/components/app/widget-card.js +2 -2
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.js +5 -2
- package/dist/components/ui/button.js +10 -10
- package/dist/components/ui/card.js +3 -3
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/combobox.js +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/sheet.js +1 -1
- package/dist/components/ui/slider.d.ts +3 -0
- package/dist/components/ui/slider.js +12 -0
- package/dist/components/ui/textarea.js +1 -1
- package/dist/hooks/use-palette.d.ts +14 -0
- package/dist/hooks/use-palette.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/palettes.d.ts +21 -0
- package/dist/lib/palettes.js +74 -0
- package/package.json +1 -1
- package/styles/index.css +60 -0
- package/styles/theme.css +321 -11
- package/vite/index.d.ts +7 -1
- package/vite/pwa.js +21 -7
|
@@ -3,25 +3,28 @@
|
|
|
3
3
|
<!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
8
|
-
- [
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [A
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
6
|
+
- [Palettes](#palettes) · lines 37–95
|
|
7
|
+
- [Glow](#glow) · lines 97–115
|
|
8
|
+
- [Semantic tones](#semantic-tones) · lines 117–145
|
|
9
|
+
- [The three badge styles](#the-three-badge-styles) · lines 147–164
|
|
10
|
+
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 166–195
|
|
11
|
+
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 197–210
|
|
12
|
+
- [Logos and brand marks](#logos-and-brand-marks) · lines 212–256
|
|
13
|
+
- [Categorical colour](#categorical-colour) · lines 258–328
|
|
14
|
+
- [Shapes and sizes](#shapes-and-sizes) · lines 330–351
|
|
15
|
+
- [Typography](#typography) · lines 353–447
|
|
16
|
+
- [Written text (notes)](#written-text-notes) · lines 387–447
|
|
17
|
+
- [Density](#density) · lines 449–506
|
|
18
|
+
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 508–622
|
|
19
|
+
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 624–693
|
|
20
|
+
- [Notices](#notices) · lines 695–723
|
|
21
|
+
- [A value over time](#a-value-over-time) · lines 725–803
|
|
22
|
+
- [Several values over time](#several-values-over-time) · lines 766–803
|
|
23
|
+
- [A list as a board](#a-list-as-a-board) · lines 805–856
|
|
24
|
+
- [A list as cards](#a-list-as-cards) · lines 858–904
|
|
25
|
+
- [Key numbers on a record](#key-numbers-on-a-record) · lines 906–928
|
|
26
|
+
- [Picking at random](#picking-at-random) · lines 930–961
|
|
27
|
+
- [Filling in what's missing](#filling-in-whats-missing) · lines 963–1030
|
|
25
28
|
|
|
26
29
|
<!-- /index -->
|
|
27
30
|
|
|
@@ -31,6 +34,86 @@ Tokens and styles here **grow freely**, in the foundation package, so every
|
|
|
31
34
|
app gets them: they do not wait for a second app to need them. New
|
|
32
35
|
composites don't grow that way; they are built against a real app's screen.
|
|
33
36
|
|
|
37
|
+
## Palettes
|
|
38
|
+
|
|
39
|
+
A palette is an app's own dark look: its surfaces, text, accent and a
|
|
40
|
+
second colour, chosen once for every app the foundation serves. There
|
|
41
|
+
are four, in the order they were ranked:
|
|
42
|
+
|
|
43
|
+
| Name | `palette` | Accent | Second colour |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| Indigo and Cyan | `indigo-cyan` | indigo, the only accent that passes 4.5:1 on cards | cyan |
|
|
46
|
+
| Indigo and Rose | `indigo-rose` | indigo-violet | rose |
|
|
47
|
+
| Graphite and Periwinkle | `graphite-periwinkle` | periwinkle on neutral grey | lilac |
|
|
48
|
+
| Violet | `violet` | violet | magenta |
|
|
49
|
+
|
|
50
|
+
**How an app picks one.** `<FoundationProviders palette="indigo-cyan">`
|
|
51
|
+
names the app's default. Add `palettePicker` and the app shell's header
|
|
52
|
+
offers all four (with swatches); the one picked is remembered on that
|
|
53
|
+
device, and a new device starts on the default. `usePalette()` reads the
|
|
54
|
+
palette on screen. Without `palette`, nothing changes: light and dark
|
|
55
|
+
follow the system, with the toggle in the header.
|
|
56
|
+
|
|
57
|
+
**A palette is dark only.** With one, the theme is forced dark and the
|
|
58
|
+
shell has no light/dark toggle. The light theme stays for apps without a
|
|
59
|
+
palette. Printed notes still print light: the print override beats a
|
|
60
|
+
palette's block.
|
|
61
|
+
|
|
62
|
+
**Where it lives.** `styles/theme.css`: each palette's colours as
|
|
63
|
+
primitives (`--palette-<name>-*`, ratios beside them), and one block per
|
|
64
|
+
palette, `.dark[data-palette="<name>"]`, that sets only what differs from
|
|
65
|
+
`.dark`. `data-palette` goes on `<html>`, so popups portaled to `<body>`
|
|
66
|
+
wear it too. An installed app's colours follow it with
|
|
67
|
+
`pwa({ palette })`.
|
|
68
|
+
|
|
69
|
+
**Why some tokens aren't the guide's colour as written.**
|
|
70
|
+
|
|
71
|
+
- **The focus ring is the accent** (4.24 to 5.25:1 on the page), not the
|
|
72
|
+
guide's accent deep, which measured 2.30 to 2.91, under the 3:1 a focus
|
|
73
|
+
indicator needs.
|
|
74
|
+
- **Links are accent light** (`--link`, 8.5:1 or more). The accent fails
|
|
75
|
+
4.5:1 as small text on Indigo and Rose and on Violet, and on every
|
|
76
|
+
palette's cards but Indigo and Cyan, so it is never small text: it is
|
|
77
|
+
the ring, the primary fill and the active item's tint. The `link`
|
|
78
|
+
Button variant is `text-link` for the same reason.
|
|
79
|
+
- **The status colours are text shades.** The guide's success, warning
|
|
80
|
+
and danger are `--success-text`, `--warning-text` and
|
|
81
|
+
`--destructive-text` (soft badges, icons, coloured words). Solid fills
|
|
82
|
+
keep the deeper shades with white text: white on the guide's success
|
|
83
|
+
green measures 1.80. Info keeps its blue.
|
|
84
|
+
- **Violet's text on its accent** is `#110823`, a shade darker than the
|
|
85
|
+
guide's `#130925`, which measured 4.496:1 (under 4.5).
|
|
86
|
+
- **Danger text** is `#df7a72`, a shade lighter than the guide's
|
|
87
|
+
`#dc726a`, which measured 4.37 on Graphite's raised surface and 4.46 on
|
|
88
|
+
an outline button's fill (an invalid image field's message).
|
|
89
|
+
- **Field borders** (`--input`, the guide's strong border) measure 2.5 to
|
|
90
|
+
2.9 against the page, under 3:1. It is already the lightest grey each
|
|
91
|
+
palette has, so it is kept as designed; axe doesn't check borders.
|
|
92
|
+
|
|
93
|
+
The package's `e2e/fixtures-visual.spec.ts` runs every primitive,
|
|
94
|
+
composite and tone story through axe in each palette, and measures every
|
|
95
|
+
glyph and categorical slot against each palette's page and card.
|
|
96
|
+
|
|
97
|
+
## Glow
|
|
98
|
+
|
|
99
|
+
Under a palette only, from `--primary` so each palette glows in its own
|
|
100
|
+
accent; an app without a palette never gets it. Everything else stays
|
|
101
|
+
flat: no glow at the top of the page.
|
|
102
|
+
|
|
103
|
+
- **The primary button** (Button's default variant) on hover, keyboard
|
|
104
|
+
focus and press: one soft layer of the accent (`--primary-glow`: 38px
|
|
105
|
+
blur, 2px spread, 38%), and on hover a shine sweeping across once
|
|
106
|
+
(650ms). Under reduced motion, no shine and no shrink. Secondary,
|
|
107
|
+
outline, ghost and destructive buttons never glow.
|
|
108
|
+
- **A hovered card**: its edge turns the accent with a soft halo
|
|
109
|
+
(`--edge-glow`), through the `glow-edge` utility. `ModuleCard`,
|
|
110
|
+
`WidgetCard` and `Board`'s cards carry it, and a `DataTable` card
|
|
111
|
+
layout's `Card`s; an app's own card takes the class.
|
|
112
|
+
- **A slider's thumb** (`ui/slider`): a small halo (`--thumb-glow`).
|
|
113
|
+
|
|
114
|
+
For anything else of an app's own, `palette:` is the variant
|
|
115
|
+
(`palette:hover:shadow-(--primary-glow)`).
|
|
116
|
+
|
|
34
117
|
## Semantic tones
|
|
35
118
|
|
|
36
119
|
Four tones carry meaning: **success**, **warning**, **destructive** and
|
|
@@ -244,6 +327,29 @@ If you find yourself wanting a thirteenth, that is a sign the column should
|
|
|
244
327
|
be showing a shape or a label rather than more colours: there is no more
|
|
245
328
|
room that stays readable.
|
|
246
329
|
|
|
330
|
+
## Shapes and sizes
|
|
331
|
+
|
|
332
|
+
The shared style guide's, the same in every app with or without a
|
|
333
|
+
palette:
|
|
334
|
+
|
|
335
|
+
- **Radius 9px** (`--radius-base`): buttons and cards on `--radius`,
|
|
336
|
+
fields on `--radius-md` (about 7px).
|
|
337
|
+
- **Buttons** 36px tall with 16px sides, semibold (`sm` 32px, `lg` 40px,
|
|
338
|
+
the icon sizes to match). A press shrinks the button to 96%, not under
|
|
339
|
+
reduced motion.
|
|
340
|
+
- **Fields** (input, select, input group, a multi-pick's box) 36px,
|
|
341
|
+
outlined. **Checkbox** 18px, a 5px radius, solid when checked, a small
|
|
342
|
+
press pop.
|
|
343
|
+
- **Cards** a hairline border with a soft shadow (`--card-shadow`: 30% in
|
|
344
|
+
dark, lighter on white), 16px padding, 14px between their parts. A
|
|
345
|
+
card's header, when it comes first, is a tinted bar flush with the top
|
|
346
|
+
(`--card-header`), as on every `WidgetCard` and `EntityView` section.
|
|
347
|
+
- **Badges** are pills; the default is a soft tint of the accent
|
|
348
|
+
(`variant="solid"` is the filled one). The tone styles are unchanged.
|
|
349
|
+
- **Behind a dialog or sheet**, the page is dimmed and blurred.
|
|
350
|
+
- **Motion** is quick: 120ms with the standard easing, for every
|
|
351
|
+
transition that doesn't name its own.
|
|
352
|
+
|
|
247
353
|
## Typography
|
|
248
354
|
|
|
249
355
|
Six roles cover every screen. Each is one class that sets size, line
|
|
@@ -251,13 +357,16 @@ height and weight together, from tokens in the package's `styles/theme.css`:
|
|
|
251
357
|
|
|
252
358
|
| Class | For | Size / weight |
|
|
253
359
|
|---|---|---|
|
|
254
|
-
| `type-page-title` | the screen's one `<h1>` (a table's title, a form's title) |
|
|
360
|
+
| `type-page-title` | the screen's one `<h1>` (a table's title, a form's title) | 28px / 600, line 1.2 |
|
|
255
361
|
| `type-section-title` | a heading inside a screen (a form section, a card group) | 16px / 600 |
|
|
256
|
-
| `type-body` | running text, table cells, form text, the pagination summary | 14px / 400 |
|
|
362
|
+
| `type-body` | running text, table cells, form text, the pagination summary | 14px / 400, line 1.5 |
|
|
257
363
|
| `type-label` | column headers, field labels, a small heading over a group | 14px / 500 |
|
|
258
364
|
| `type-caption` | counts, metadata, helper text under a field | 12px / 400 |
|
|
259
365
|
| `type-numeric` | the one live number on a card (`ModuleCard`'s count), in tabular figures | 22px / 500 |
|
|
260
366
|
|
|
367
|
+
- **Headings draw in a little:** the page and section titles, and written
|
|
368
|
+
text's `#` and `##`, take -0.02em letter spacing. Labels stay normal
|
|
369
|
+
case at 14px medium (small caps at 12px read too small).
|
|
261
370
|
- **Use a role instead of size and weight classes,** not as well as them.
|
|
262
371
|
`type-page-title` replaces `text-lg font-semibold`. Combining a role with
|
|
263
372
|
`text-sm`, `font-medium` or `leading-*` makes two classes set the same
|
|
@@ -351,8 +460,8 @@ set, not a `DataTable` prop:
|
|
|
351
460
|
|
|
352
461
|
| `data-density` | Changes | Table cell padding (vertical) | Header height | For |
|
|
353
462
|
|---|---|---|---|---|
|
|
354
|
-
|
|
|
355
|
-
|
|
|
463
|
+
| *(unset)* | — | 4px | 32px | the default: compact rows, as the shared style guide has them |
|
|
464
|
+
| `compact` | a hub's cards (`WidgetCard`, `WidgetList` rows, `ModuleCard`); table rows stay the default's | 4px | 32px | a dense hub |
|
|
356
465
|
| `comfortable` | table rows only | 12px | 48px | short tables, or rows people read rather than scan |
|
|
357
466
|
| `large` | **everything**: type, spacing, control heights, icons, the sidebar, table rows | 12px | 56px | a screen read from a distance: a TV used as the desktop monitor, with a mouse and keyboard |
|
|
358
467
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ChevronRightIcon, LogOutIcon, MoonIcon, SunIcon } from 'lucide-react';
|
|
2
|
+
import { ChevronRightIcon, LogOutIcon, MoonIcon, PaletteIcon, SunIcon } from 'lucide-react';
|
|
3
3
|
import { useTheme } from 'next-themes';
|
|
4
4
|
import { useEffect, useId, useState } from 'react';
|
|
5
5
|
import { Link, Navigate, Outlet, useLocation } from 'react-router';
|
|
@@ -11,8 +11,11 @@ import { ErrorState } from './error-state.js';
|
|
|
11
11
|
import { useErrorReporter } from '../../error-reporting/error-reporting-context.js';
|
|
12
12
|
import { ReportRoutePattern } from '../../error-reporting/error-reporting-provider.js';
|
|
13
13
|
import { useShellDocumentTitle } from '../../hooks/use-document-title.js';
|
|
14
|
+
import { usePalette } from '../../hooks/use-palette.js';
|
|
15
|
+
import { isPaletteName, PALETTE_LABELS, PALETTES } from '../../lib/palettes.js';
|
|
14
16
|
import { cn } from 'cn';
|
|
15
17
|
import { Button } from '../ui/button.js';
|
|
18
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
|
|
16
19
|
import { Skeleton } from '../ui/skeleton.js';
|
|
17
20
|
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarTrigger, useSidebar, } from '../ui/sidebar.js';
|
|
18
21
|
import { Toaster } from '../ui/sonner.js';
|
|
@@ -36,6 +39,29 @@ function CloseMobileSidebarOnNavigate() {
|
|
|
36
39
|
}, [key, setOpenMobile]);
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
42
|
+
// The header's look control: an app with a palette is always dark, so it
|
|
43
|
+
// has no light/dark toggle, and with the picker on it gets the palette
|
|
44
|
+
// menu in its place (design-language.md "Palettes").
|
|
45
|
+
function LookControl() {
|
|
46
|
+
const { palette, picker } = usePalette();
|
|
47
|
+
if (!palette)
|
|
48
|
+
return _jsx(ThemeToggle, {});
|
|
49
|
+
return picker ? _jsx(PalettePicker, { palette: palette }) : null;
|
|
50
|
+
}
|
|
51
|
+
// Each palette's swatch is its own background with its accent and second
|
|
52
|
+
// colour on it, read from the palette primitives, so the menu shows all
|
|
53
|
+
// four whichever is on screen.
|
|
54
|
+
function PaletteSwatch({ name }) {
|
|
55
|
+
const colour = (part) => `var(--palette-${name}-${part})`;
|
|
56
|
+
return (_jsxs("span", { "aria-hidden": true, className: "flex h-4 w-7 shrink-0 items-center justify-center gap-0.5 rounded-sm border border-border", style: { background: colour('background') }, children: [_jsx("span", { className: "size-2 rounded-full", style: { background: colour('accent') } }), _jsx("span", { className: "size-2 rounded-full", style: { background: colour('second') } })] }));
|
|
57
|
+
}
|
|
58
|
+
function PalettePicker({ palette }) {
|
|
59
|
+
const { setPalette } = usePalette();
|
|
60
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `Palette: ${PALETTE_LABELS[palette]}` }), children: _jsx(PaletteIcon, {}) }), _jsx(DropdownMenuContent, { align: "end", className: "w-60", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Palette on this device" }), _jsx(DropdownMenuRadioGroup, { value: palette, onValueChange: (next) => {
|
|
61
|
+
if (isPaletteName(next))
|
|
62
|
+
setPalette(next);
|
|
63
|
+
}, children: PALETTES.map((name) => (_jsxs(DropdownMenuRadioItem, { value: name, children: [_jsx(PaletteSwatch, { name: name }), PALETTE_LABELS[name]] }, name))) })] }) })] }));
|
|
64
|
+
}
|
|
39
65
|
function ThemeToggle() {
|
|
40
66
|
const { resolvedTheme, setTheme } = useTheme();
|
|
41
67
|
const isDark = resolvedTheme === 'dark';
|
|
@@ -160,5 +186,5 @@ export function AppShell({ title, logo: Logo, nav, sidebarExtra, defaultSidebarO
|
|
|
160
186
|
// (AuthProvider), so this stays a deliberate logout: a
|
|
161
187
|
// clean /login, never the previous user's page as `from`.
|
|
162
188
|
logout().catch(() => { });
|
|
163
|
-
}, children: _jsx(LogOutIcon, {}) })] })] })] }) }), _jsxs(SidebarInset, { className: "min-w-0", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-4", children: [_jsx(SidebarTrigger, {}), _jsx("div", { className: "flex-1" }), _jsx(
|
|
189
|
+
}, children: _jsx(LogOutIcon, {}) })] })] })] }) }), _jsxs(SidebarInset, { className: "min-w-0", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-4", children: [_jsx(SidebarTrigger, {}), _jsx("div", { className: "flex-1" }), _jsx(LookControl, {})] }), _jsx("div", { className: cn('flex-1 p-6', bottomBar?.length && BOTTOM_BAR_PADDING), children: _jsx(Outlet, {}) })] }), bottomBar && bottomBar.length > 0 && _jsx(BottomBar, { items: bottomBar, nav: nav, activeTo: activeTo })] }), _jsx(Toaster, { position: "top-center" })] }));
|
|
164
190
|
}
|
|
@@ -101,7 +101,7 @@ export function Board({ 'aria-label': ariaLabel, columns, items, getId, getColum
|
|
|
101
101
|
event.dataTransfer.setData(DRAG_TYPE, String(getId(item)));
|
|
102
102
|
event.dataTransfer.effectAllowed = 'move';
|
|
103
103
|
}
|
|
104
|
-
: undefined, className: cn('gap-2 px-3 py-3', onMove && 'cursor-grab active:cursor-grabbing'), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: renderCard(item) }), onMove && (_jsx(MoveMenu, { id: String(getId(item)), name: getName(item), value: getColumn(item), targets: targets, onMove: (value) => move(String(getId(item)), value, 'menu') }))] }) }, getId(item)))), cards.length === 0 && (_jsx("p", { className: "px-1 py-6 text-center type-caption text-muted-foreground", children: column.empty ?? 'Nothing here' })), column.footer && _jsx("div", { className: "px-1 type-caption text-muted-foreground", children: column.footer })] }, column.value));
|
|
104
|
+
: undefined, className: cn('glow-edge gap-2 px-3 py-3', onMove && 'cursor-grab active:cursor-grabbing'), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: renderCard(item) }), onMove && (_jsx(MoveMenu, { id: String(getId(item)), name: getName(item), value: getColumn(item), targets: targets, onMove: (value) => move(String(getId(item)), value, 'menu') }))] }) }, getId(item)))), cards.length === 0 && (_jsx("p", { className: "px-1 py-6 text-center type-caption text-muted-foreground", children: column.empty ?? 'Nothing here' })), column.footer && _jsx("div", { className: "px-1 type-caption text-muted-foreground", children: column.footer })] }, column.value));
|
|
105
105
|
}), _jsx("span", { role: "status", className: "sr-only", children: announcement })] }));
|
|
106
106
|
}
|
|
107
107
|
function BoardColumnView({ column, count, headingLevel, onDrop, children, }) {
|
|
@@ -559,7 +559,7 @@ function GroupLabel({ run, icon, isFolded, onToggle, }) {
|
|
|
559
559
|
return (_jsxs("button", { type: "button", "aria-expanded": !isFolded, onClick: onToggle, className: GROUP_TOGGLE_CLASS, children: [_jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('size-4 self-center text-muted-foreground transition-transform', isFolded && '-rotate-90') }), label, _jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: run.rows.length })] }));
|
|
560
560
|
}
|
|
561
561
|
function Cards({ layout, runs, rows, folded, onToggleFold, groupSummary, groupIcons, }) {
|
|
562
|
-
const list = (items) => (_jsx("ul", { role: "list", className: cn(layout.arrange === 'list' ? CARD_LIST_CLASS : ['grid', CARD_GRID_CLASS[layout.size ?? 'md'], CARD_GRID_GAP_CLASS]), children: items.map((row) => (_jsx("li", { "data-slot": "data-table-card", className: "min-w-0", children: layout.renderCard(row.original) }, row.id))) }));
|
|
562
|
+
const list = (items) => (_jsx("ul", { role: "list", className: cn(layout.arrange === 'list' ? CARD_LIST_CLASS : ['grid', CARD_GRID_CLASS[layout.size ?? 'md'], CARD_GRID_GAP_CLASS]), children: items.map((row) => (_jsx("li", { "data-slot": "data-table-card", className: "min-w-0 *:data-[slot=card]:glow-edge", children: layout.renderCard(row.original) }, row.id))) }));
|
|
563
563
|
return (_jsx("div", { className: "@container/data-table-cards flex flex-col gap-6", "data-state": "success", "data-slot": "data-table-cards", children: runs
|
|
564
564
|
? runs.map((run, runIndex) => {
|
|
565
565
|
const isFolded = folded.has(run.key);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import type { PaletteName } from '../../lib/palettes.js';
|
|
3
4
|
import { type ErrorReportingOptions } from '../../error-reporting/reporter.js';
|
|
4
|
-
export declare function FoundationProviders({ mockMode, queryClient, errorReporting, children, }: {
|
|
5
|
+
export declare function FoundationProviders({ mockMode, queryClient, errorReporting, palette, palettePicker, children, }: {
|
|
5
6
|
/** Whether the API is served by MSW — the app's own IS_MOCK_MODE. */
|
|
6
7
|
mockMode: boolean;
|
|
7
8
|
/** Defaults to createQueryClient(); pass one to share it with code outside React. */
|
|
@@ -13,5 +14,17 @@ export declare function FoundationProviders({ mockMode, queryClient, errorReport
|
|
|
13
14
|
* report holds and never holds. `{ release }` names the build.
|
|
14
15
|
*/
|
|
15
16
|
errorReporting?: boolean | ErrorReportingOptions;
|
|
17
|
+
/**
|
|
18
|
+
* The app's palette (design-language.md "Palettes"). With one, the app
|
|
19
|
+
* is always dark in that palette and the shell has no light/dark
|
|
20
|
+
* toggle; printed notes still print light. Without, light and dark
|
|
21
|
+
* follow the system, as before.
|
|
22
|
+
*/
|
|
23
|
+
palette?: PaletteName;
|
|
24
|
+
/**
|
|
25
|
+
* With `palette`: the shell offers all four palettes, and the one picked
|
|
26
|
+
* is remembered on this device. A new device starts on `palette`.
|
|
27
|
+
*/
|
|
28
|
+
palettePicker?: boolean;
|
|
16
29
|
children: ReactNode;
|
|
17
30
|
}): import("react").JSX.Element;
|
|
@@ -5,20 +5,21 @@ import { createQueryClient } from '../../api/query-client.js';
|
|
|
5
5
|
import { AuthProvider } from '../../auth/use-auth.js';
|
|
6
6
|
import { MockModeBanner } from './mock-mode-banner.js';
|
|
7
7
|
import { ThemeProvider } from '../theme-provider.js';
|
|
8
|
+
import { PaletteProvider } from './palette-provider.js';
|
|
8
9
|
import { ErrorReportingProvider } from '../../error-reporting/error-reporting-provider.js';
|
|
9
10
|
import { resolveErrorReporting } from '../../error-reporting/reporter.js';
|
|
10
11
|
import { browserStaleChunkReload } from '../../lib/stale-chunk-reload.js';
|
|
11
12
|
// Everything an app's root needs around its router, in the order that
|
|
12
|
-
// works: the theme outermost, the mock-mode banner above the router so it
|
|
13
|
+
// works: the theme outermost (and the palette, which forces it dark), the mock-mode banner above the router so it
|
|
13
14
|
// shows on every route (/login included), and AuthProvider inside
|
|
14
15
|
// QueryClientProvider because it queries /auth/me through TanStack Query.
|
|
15
16
|
// An app's DataEnvironmentBanner goes first among the children, before its
|
|
16
17
|
// router: it queries too, and shows on every route like the mock banner.
|
|
17
18
|
// It also reloads the page once when a lazy chunk is gone after a deploy
|
|
18
19
|
// (lib/stale-chunk-reload.ts), so no app adds its own listener.
|
|
19
|
-
export function FoundationProviders({ mockMode, queryClient, errorReporting, children, }) {
|
|
20
|
+
export function FoundationProviders({ mockMode, queryClient, errorReporting, palette, palettePicker = false, children, }) {
|
|
20
21
|
const [client] = useState(() => queryClient ?? createQueryClient());
|
|
21
22
|
const reporting = resolveErrorReporting(errorReporting);
|
|
22
23
|
useEffect(() => browserStaleChunkReload().install(window), []);
|
|
23
|
-
return (
|
|
24
|
+
return (_jsx(ThemeProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, forcedTheme: palette ? 'dark' : undefined, children: _jsxs(PaletteProvider, { palette: palette, picker: palettePicker, children: [_jsx(MockModeBanner, { active: mockMode }), _jsx(QueryClientProvider, { client: client, children: _jsx(AuthProvider, { children: reporting ? _jsx(ErrorReportingProvider, { options: reporting, children: children }) : children }) })] }) }));
|
|
24
25
|
}
|
|
@@ -113,7 +113,7 @@ export function ModuleCard({ title, icon: Icon, category, to, href, value, note,
|
|
|
113
113
|
'data-slot': 'module-card-band',
|
|
114
114
|
className: cn(BAND_LINK_CLASS, category && BAND_CLASS[category]),
|
|
115
115
|
};
|
|
116
|
-
const card = (_jsxs(Card, { "data-slot": "module-card", "data-state": state, className: cn('relative h-full min-w-0 gap-0 py-0', mobile === 'row' && 'max-md:hidden', className), children: [href !== undefined ? (_jsxs("a", { href: href, target: "_blank", rel: "noreferrer", ...bandProps, children: [band, _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: to, ...bandProps, children: band })), _jsxs("div", { id: bodyId, "data-slot": "module-card-body", "aria-busy": state === 'loading' || undefined, className: "flex flex-1 flex-col gap-0.5 px-3 py-2 density-compact:py-1.5 max-sm:px-2", children: [state === 'error' ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "flex items-center gap-1.5 type-body text-foreground", children: [_jsx(AlertCircleIcon, { "aria-hidden": "true", className: "size-4 shrink-0 text-destructive-text" }), _jsxs("span", { className: "min-w-0", children: ["Couldn't load", error?.message && _jsxs("span", { className: "sr-only", children: [": ", error.message] })] })] }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, "aria-describedby": titleId, className: "relative z-10 mt-1 self-start", children: "Retry" }))] })) : state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "Loading" }), _jsx(Skeleton, { "data-state": "loading", className: "my-0.5 h-6 w-12" })] })) : (value !== undefined && (_jsx("span", { "data-slot": "module-card-value", className: "type-numeric text-foreground", children: value }))), note && state !== 'error' && (_jsx("span", { className: "truncate type-body text-muted-foreground", title: note, children: note }))] }), action &&
|
|
116
|
+
const card = (_jsxs(Card, { "data-slot": "module-card", "data-state": state, className: cn('glow-edge relative h-full min-w-0 gap-0 py-0', mobile === 'row' && 'max-md:hidden', className), children: [href !== undefined ? (_jsxs("a", { href: href, target: "_blank", rel: "noreferrer", ...bandProps, children: [band, _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: to, ...bandProps, children: band })), _jsxs("div", { id: bodyId, "data-slot": "module-card-body", "aria-busy": state === 'loading' || undefined, className: "flex flex-1 flex-col gap-0.5 px-3 py-2 density-compact:py-1.5 max-sm:px-2", children: [state === 'error' ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "flex items-center gap-1.5 type-body text-foreground", children: [_jsx(AlertCircleIcon, { "aria-hidden": "true", className: "size-4 shrink-0 text-destructive-text" }), _jsxs("span", { className: "min-w-0", children: ["Couldn't load", error?.message && _jsxs("span", { className: "sr-only", children: [": ", error.message] })] })] }), onRetry && (_jsx(Button, { size: "sm", variant: "outline", onClick: onRetry, "aria-describedby": titleId, className: "relative z-10 mt-1 self-start", children: "Retry" }))] })) : state === 'loading' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sr-only", children: "Loading" }), _jsx(Skeleton, { "data-state": "loading", className: "my-0.5 h-6 w-12" })] })) : (value !== undefined && (_jsx("span", { "data-slot": "module-card-value", className: "type-numeric text-foreground", children: value }))), note && state !== 'error' && (_jsx("span", { className: "truncate type-body text-muted-foreground", title: note, children: note }))] }), action &&
|
|
117
117
|
(action.href !== undefined ? (_jsxs("a", { href: action.href, target: "_blank", rel: "noreferrer", "aria-describedby": titleId, className: ACTION_CLASS, children: [_jsx("span", { className: "truncate", children: action.label }), _jsx(NewTabMark, {})] })) : (_jsx(Link, { to: action.to, "aria-describedby": titleId, className: ACTION_CLASS, children: _jsx("span", { className: "truncate", children: action.label }) })))] }));
|
|
118
118
|
if (mobile === 'tiles')
|
|
119
119
|
return card;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type PaletteName, type PaletteStore } from '../../lib/palettes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Puts the palette on <html>. FoundationProviders renders it; an app
|
|
5
|
+
* doesn't. Without `palette` it does nothing, and the app keeps light and
|
|
6
|
+
* dark.
|
|
7
|
+
*/
|
|
8
|
+
export declare function PaletteProvider({ palette: defaultPalette, picker, store, children, }: {
|
|
9
|
+
palette?: PaletteName;
|
|
10
|
+
picker?: boolean;
|
|
11
|
+
/** For tests; the browser's localStorage by default. */
|
|
12
|
+
store?: PaletteStore;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useLayoutEffect, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { PaletteContext } from '../../hooks/use-palette.js';
|
|
4
|
+
import { browserPaletteStore, subscribeToBrowserPaletteStore } from '../../lib/palettes.js';
|
|
5
|
+
/**
|
|
6
|
+
* Puts the palette on <html>. FoundationProviders renders it; an app
|
|
7
|
+
* doesn't. Without `palette` it does nothing, and the app keeps light and
|
|
8
|
+
* dark.
|
|
9
|
+
*/
|
|
10
|
+
export function PaletteProvider({ palette: defaultPalette, picker = false, store = browserPaletteStore, children, }) {
|
|
11
|
+
const subscribe = useCallback((listener) => (store === browserPaletteStore ? subscribeToBrowserPaletteStore(listener) : store.subscribe(listener)), [store]);
|
|
12
|
+
const saved = useSyncExternalStore(subscribe, store.get, () => null);
|
|
13
|
+
const palette = defaultPalette ? ((picker && saved) || defaultPalette) : null;
|
|
14
|
+
// Before paint, so the first frame is already in the palette.
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
if (palette)
|
|
18
|
+
root.dataset.palette = palette;
|
|
19
|
+
else
|
|
20
|
+
delete root.dataset.palette;
|
|
21
|
+
return () => {
|
|
22
|
+
delete root.dataset.palette;
|
|
23
|
+
};
|
|
24
|
+
}, [palette]);
|
|
25
|
+
const value = useMemo(() => ({
|
|
26
|
+
palette,
|
|
27
|
+
defaultPalette: defaultPalette ?? null,
|
|
28
|
+
picker: Boolean(defaultPalette && picker),
|
|
29
|
+
setPalette: (next) => {
|
|
30
|
+
if (defaultPalette && picker)
|
|
31
|
+
store.set(next);
|
|
32
|
+
},
|
|
33
|
+
}), [palette, defaultPalette, picker, store]);
|
|
34
|
+
return _jsx(PaletteContext.Provider, { value: value, children: children });
|
|
35
|
+
}
|
|
@@ -90,7 +90,7 @@ export function WidgetCard({ id, title, icon: Icon, headingLevel = 2, link, isLo
|
|
|
90
90
|
return;
|
|
91
91
|
toggle();
|
|
92
92
|
};
|
|
93
|
-
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: "density-compact:[--card-spacing:--spacing(3)]", children: [_jsxs(CardHeader, { onClick: onHeaderClick, className: cn('flex items-center justify-between gap-3', inAccordion && 'cursor-pointer select-none'), children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || foldable || summaryShown) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [summaryShown && (_jsx("span", { id: summaryId, "data-slot": "widget-card-summary", className: "mr-2 flex items-center", children: _jsx(WidgetCardSummary, { state: state, summary: summary, emptySummary: emptySummary }) })), link?.href !== undefined ? (_jsxs("a", { href: link.href, target: "_blank", rel: "noreferrer", "aria-describedby": headingId, className: cn(HEADER_LINK_CLASS, 'inline-flex items-center gap-1'), children: [link.label, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))), foldable && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-describedby": summaryShown ? summaryId : undefined, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: toggle,
|
|
93
|
+
return (_jsx("section", { id: id, "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: "glow-edge density-compact:[--card-spacing:--spacing(3)]", children: [_jsxs(CardHeader, { onClick: onHeaderClick, className: cn('flex items-center justify-between gap-3', inAccordion && 'cursor-pointer select-none'), children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || foldable || summaryShown) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [summaryShown && (_jsx("span", { id: summaryId, "data-slot": "widget-card-summary", className: "mr-2 flex items-center", children: _jsx(WidgetCardSummary, { state: state, summary: summary, emptySummary: emptySummary }) })), link?.href !== undefined ? (_jsxs("a", { href: link.href, target: "_blank", rel: "noreferrer", "aria-describedby": headingId, className: cn(HEADER_LINK_CLASS, 'inline-flex items-center gap-1'), children: [link.label, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))), foldable && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-describedby": summaryShown ? summaryId : undefined, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: toggle,
|
|
94
94
|
// The button's own open look (a filled background) would
|
|
95
95
|
// read as pressed: the chevron says it.
|
|
96
96
|
className: "-my-1 -mr-1.5 aria-expanded:not-hover:bg-transparent", children: _jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('transition-transform', collapsed && '-rotate-90') }) }))] }))] }), _jsx(CardContent, { id: contentId, hidden: collapsed, "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
|
@@ -127,7 +127,7 @@ const ROW_CLASS = 'flex min-h-9 items-center gap-3 py-2 density-compact:min-h-8
|
|
|
127
127
|
// Varied widths, so the skeleton reads as a list of titles, not a grid.
|
|
128
128
|
const SKELETON_WIDTHS = ['w-3/5', 'w-2/5', 'w-1/2', 'w-2/3', 'w-1/3', 'w-3/5'];
|
|
129
129
|
function WidgetListSkeleton({ rows }) {
|
|
130
|
-
return (_jsxs("div", { "data-state": "loading", className: "flex flex-col divide-y divide-border", children: [_jsx("span", { className: "sr-only", children: "Loading" }), Array.from({ length: rows }, (_, row) => (_jsxs("div", { className: ROW_CLASS, children: [_jsx(Skeleton, { className: cn('h-4', SKELETON_WIDTHS[row % SKELETON_WIDTHS.length]) }), _jsx(Skeleton, { className: "ml-auto h-4 w-12" })] }, row)))] }));
|
|
130
|
+
return (_jsxs("div", { "data-state": "loading", className: "flex flex-col divide-y divide-border", children: [_jsx("span", { className: "sr-only", children: "Loading" }), Array.from({ length: rows }, (_, row) => (_jsxs("div", { className: cn(ROW_CLASS, 'type-body'), children: [_jsx("span", { "aria-hidden": "true", className: "-mr-3 w-0 overflow-hidden", children: "\u00A0" }), _jsx(Skeleton, { className: cn('h-4', SKELETON_WIDTHS[row % SKELETON_WIDTHS.length]) }), _jsx(Skeleton, { className: "ml-auto h-4 w-12" })] }, row)))] }));
|
|
131
131
|
}
|
|
132
132
|
/** The rows of a WidgetCard: a list with a divider between rows. */
|
|
133
133
|
export function WidgetList({ children, className }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRender } from "@base-ui/react/use-render";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: ({
|
|
4
|
-
variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "warning" | "info" | "outline-success" | "outline-warning" | "outline-info" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-info" | "tinted-destructive" | null | undefined;
|
|
4
|
+
variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "solid" | "warning" | "info" | "outline-success" | "outline-warning" | "outline-info" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-info" | "tinted-destructive" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
declare function Badge({ className, variant, render, ...props }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
7
7
|
export { Badge, badgeVariants };
|
|
@@ -5,7 +5,10 @@ import { cn } from "cn";
|
|
|
5
5
|
const badgeVariants = cva("group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", {
|
|
6
6
|
variants: {
|
|
7
7
|
variant: {
|
|
8
|
-
default
|
|
8
|
+
// The default is a soft tint of the accent (the shared style
|
|
9
|
+
// guide's badge); `solid` is the old filled look.
|
|
10
|
+
default: "bg-primary/12 text-foreground dark:bg-primary/20 dark:text-link [a]:hover:bg-primary/20",
|
|
11
|
+
solid: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
9
12
|
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
10
13
|
destructive: "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/90",
|
|
11
14
|
success: "bg-success text-success-foreground focus-visible:ring-success/20 dark:focus-visible:ring-success/40 [a]:hover:bg-success/90",
|
|
@@ -30,7 +33,7 @@ const badgeVariants = cva("group/badge inline-flex h-5 w-fit shrink-0 items-cent
|
|
|
30
33
|
"tinted-info": "bg-info/15 text-info-text dark:bg-info/20",
|
|
31
34
|
"tinted-destructive": "bg-destructive/15 text-destructive-text dark:bg-destructive/20",
|
|
32
35
|
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
33
|
-
link: "text-
|
|
36
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
34
37
|
},
|
|
35
38
|
},
|
|
36
39
|
defaultVariants: {
|
|
@@ -2,25 +2,25 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Button as ButtonPrimitive } from "@base-ui/react/button";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
4
|
import { cn } from "cn";
|
|
5
|
-
const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-
|
|
5
|
+
const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-semibold whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform] outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 motion-safe:active:not-aria-[haspopup]:scale-96 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
|
|
6
6
|
variants: {
|
|
7
7
|
variant: {
|
|
8
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
|
8
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/80 palette:hover:shadow-(--primary-glow) palette:focus-visible:shadow-(--primary-glow) palette:active:shadow-(--primary-glow)",
|
|
9
9
|
outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
10
10
|
secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
11
11
|
ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
12
12
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
13
|
-
link: "text-
|
|
13
|
+
link: "text-link underline-offset-4 hover:underline",
|
|
14
14
|
},
|
|
15
15
|
size: {
|
|
16
|
-
default: "h-
|
|
16
|
+
default: "h-9 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
17
17
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
18
|
-
sm: "h-
|
|
19
|
-
lg: "h-
|
|
20
|
-
icon: "size-
|
|
18
|
+
sm: "h-8 gap-1 rounded-[min(var(--radius-md),12px)] px-3 text-(length:--control-text-sm) in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
19
|
+
lg: "h-10 gap-1.5 px-5 has-data-[icon=inline-end]:pr-4 has-data-[icon=inline-start]:pl-4",
|
|
20
|
+
icon: "size-9",
|
|
21
21
|
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
22
|
-
"icon-sm": "size-
|
|
23
|
-
"icon-lg": "size-
|
|
22
|
+
"icon-sm": "size-8 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
23
|
+
"icon-lg": "size-10",
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
defaultVariants: {
|
|
@@ -29,6 +29,6 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
|
|
|
29
29
|
},
|
|
30
30
|
});
|
|
31
31
|
function Button({ className, variant = "default", size = "default", ...props }) {
|
|
32
|
-
return (_jsx(ButtonPrimitive, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props }));
|
|
32
|
+
return (_jsx(ButtonPrimitive, { "data-slot": "button", "data-variant": variant, className: cn(buttonVariants({ variant, size, className })), ...props }));
|
|
33
33
|
}
|
|
34
34
|
export { Button, buttonVariants };
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cn } from "cn";
|
|
4
4
|
function Card({ className, size = "default", ...props }) {
|
|
5
|
-
return (_jsx("div", { "data-slot": "card", "data-size": size, className: cn("group/card flex flex-col gap-(--card-
|
|
5
|
+
return (_jsx("div", { "data-slot": "card", "data-size": size, className: cn("group/card flex flex-col gap-(--card-gap) overflow-hidden rounded-lg bg-card py-(--card-spacing) text-sm text-card-foreground shadow-(--card-shadow) ring-1 ring-border [--card-gap:--spacing(3.5)] [--card-spacing:--spacing(4)] has-[>[data-slot=card-header]:first-child]:pt-0 has-[>[data-slot=card-header]:last-child]:pb-0 data-[size=sm]:[--card-gap:--spacing(3)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg", className), ...props }));
|
|
6
6
|
}
|
|
7
7
|
function CardHeader({ className, ...props }) {
|
|
8
|
-
return (_jsx("div", { "data-slot": "card-header", className: cn("group/card-header @container/card-header grid auto-rows-min items-start gap-1
|
|
8
|
+
return (_jsx("div", { "data-slot": "card-header", className: cn("group/card-header @container/card-header grid auto-rows-min items-start gap-1 px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing) first:border-b first:border-border first:bg-card-header first:py-[calc(var(--card-spacing)*0.75)] last:border-b-0", className), ...props }));
|
|
9
9
|
}
|
|
10
10
|
function CardTitle({ className, ...props }) {
|
|
11
11
|
return (_jsx("div", { "data-slot": "card-title", className: cn("font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className), ...props }));
|
|
@@ -20,6 +20,6 @@ function CardContent({ className, ...props }) {
|
|
|
20
20
|
return (_jsx("div", { "data-slot": "card-content", className: cn("px-(--card-spacing)", className), ...props }));
|
|
21
21
|
}
|
|
22
22
|
function CardFooter({ className, ...props }) {
|
|
23
|
-
return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center
|
|
23
|
+
return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center border-t bg-muted/50 p-(--card-spacing)", className), ...props }));
|
|
24
24
|
}
|
|
25
25
|
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
|
|
@@ -3,6 +3,6 @@ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
|
3
3
|
import { cn } from "cn";
|
|
4
4
|
import { CheckIcon } from "lucide-react";
|
|
5
5
|
function Checkbox({ className, ...props }) {
|
|
6
|
-
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer relative flex size-4 shrink-0 items-center justify-center rounded-[
|
|
6
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer relative flex size-4.5 shrink-0 items-center justify-center rounded-[5px] border border-input transition-[color,background-color,border-color,box-shadow,transform] outline-none motion-safe:active:scale-85 group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) }) }));
|
|
7
7
|
}
|
|
8
8
|
export { Checkbox };
|
|
@@ -44,7 +44,7 @@ function ComboboxSeparator({ className, ...props }) {
|
|
|
44
44
|
return (_jsx(ComboboxPrimitive.Separator, { "data-slot": "combobox-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
|
|
45
45
|
}
|
|
46
46
|
function ComboboxChips({ className, ...props }) {
|
|
47
|
-
return (_jsx(ComboboxPrimitive.Chips, { "data-slot": "combobox-chips", className: cn("flex min-h-
|
|
47
|
+
return (_jsx(ComboboxPrimitive.Chips, { "data-slot": "combobox-chips", className: cn("flex min-h-9 flex-wrap items-center gap-1 rounded-md border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40", className), ...props }));
|
|
48
48
|
}
|
|
49
49
|
function ComboboxChip({ className, children, showRemove = true, ...props }) {
|
|
50
50
|
return (_jsxs(ComboboxPrimitive.Chip, { "data-slot": "combobox-chip", className: cn("flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0", className), ...props, children: [children, showRemove && (_jsx(ComboboxPrimitive.ChipRemove
|
|
@@ -17,7 +17,7 @@ function DialogClose({ ...props }) {
|
|
|
17
17
|
return _jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
|
|
18
18
|
}
|
|
19
19
|
function DialogOverlay({ className, ...props }) {
|
|
20
|
-
return (_jsx(DialogPrimitive.Backdrop, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 isolate z-50 bg-black/
|
|
20
|
+
return (_jsx(DialogPrimitive.Backdrop, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 isolate z-50 bg-black/20 duration-100 supports-backdrop-filter:backdrop-blur-sm dark:bg-black/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", className), ...props }));
|
|
21
21
|
}
|
|
22
22
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
23
23
|
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Popup, { "data-slot": "dialog-content", className: cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: [children, showCloseButton && (_jsxs(DialogPrimitive.Close, { "data-slot": "dialog-close", render: _jsx(Button, { variant: "ghost", className: "absolute top-2 right-2", size: "icon-sm" }), children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }))] })] }));
|
|
@@ -6,7 +6,7 @@ import { Button } from "./button.js";
|
|
|
6
6
|
import { Input } from "./input.js";
|
|
7
7
|
import { Textarea } from "./textarea.js";
|
|
8
8
|
function InputGroup({ className, ...props }) {
|
|
9
|
-
return (_jsx("div", { "data-slot": "input-group", role: "group", className: cn("group/input-group relative flex h-
|
|
9
|
+
return (_jsx("div", { "data-slot": "input-group", role: "group", className: cn("group/input-group relative flex h-9 w-full min-w-0 items-center rounded-md border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 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 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 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", className), ...props }));
|
|
10
10
|
}
|
|
11
11
|
const inputGroupAddonVariants = cva("flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", {
|
|
12
12
|
variants: {
|