@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.
Files changed (37) hide show
  1. package/conventions/docs/design-language.md +132 -23
  2. package/dist/components/app/app-shell.js +28 -2
  3. package/dist/components/app/board.js +1 -1
  4. package/dist/components/app/data-table.js +1 -1
  5. package/dist/components/app/foundation-providers.d.ts +14 -1
  6. package/dist/components/app/foundation-providers.js +4 -3
  7. package/dist/components/app/module-card.js +1 -1
  8. package/dist/components/app/palette-provider.d.ts +14 -0
  9. package/dist/components/app/palette-provider.js +35 -0
  10. package/dist/components/app/widget-card.js +2 -2
  11. package/dist/components/ui/badge.d.ts +1 -1
  12. package/dist/components/ui/badge.js +5 -2
  13. package/dist/components/ui/button.js +10 -10
  14. package/dist/components/ui/card.js +3 -3
  15. package/dist/components/ui/checkbox.js +1 -1
  16. package/dist/components/ui/combobox.js +1 -1
  17. package/dist/components/ui/dialog.js +1 -1
  18. package/dist/components/ui/input-group.js +1 -1
  19. package/dist/components/ui/input.js +1 -1
  20. package/dist/components/ui/progress.d.ts +7 -0
  21. package/dist/components/ui/progress.js +19 -0
  22. package/dist/components/ui/select.js +1 -1
  23. package/dist/components/ui/sheet.js +1 -1
  24. package/dist/components/ui/slider.d.ts +3 -0
  25. package/dist/components/ui/slider.js +12 -0
  26. package/dist/components/ui/textarea.js +1 -1
  27. package/dist/hooks/use-palette.d.ts +14 -0
  28. package/dist/hooks/use-palette.js +7 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +2 -0
  31. package/dist/lib/palettes.d.ts +21 -0
  32. package/dist/lib/palettes.js +74 -0
  33. package/package.json +1 -1
  34. package/styles/index.css +60 -0
  35. package/styles/theme.css +321 -11
  36. package/vite/index.d.ts +7 -1
  37. 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
- - [Semantic tones](#semantic-tones) · lines 34–62
7
- - [The three badge styles](#the-three-badge-styles) · lines 64–81
8
- - [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 83–112
9
- - [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 114–127
10
- - [Logos and brand marks](#logos-and-brand-marks) · lines 129–173
11
- - [Categorical colour](#categorical-colour) · lines 175–245
12
- - [Typography](#typography) · lines 247–338
13
- - [Written text (notes)](#written-text-notes) · lines 278–338
14
- - [Density](#density) · lines 340–397
15
- - [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 399–513
16
- - [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 515–584
17
- - [Notices](#notices) · lines 586–614
18
- - [A value over time](#a-value-over-time) · lines 616–694
19
- - [Several values over time](#several-values-over-time) · lines 657–694
20
- - [A list as a board](#a-list-as-a-board) · lines 696–747
21
- - [A list as cards](#a-list-as-cards) · lines 749–795
22
- - [Key numbers on a record](#key-numbers-on-a-record) · lines 797–819
23
- - [Picking at random](#picking-at-random) · lines 821–852
24
- - [Filling in what's missing](#filling-in-whats-missing) · lines 854–921
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) | 18px / 600 |
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
- | `compact` | table rows, and a hub's cards (`WidgetCard`, `WidgetList` rows, `ModuleCard`) | 4px | 32px | wide tables people scan, many rows per screen; a dense hub |
355
- | *(unset)* | — | 8px | 40px | the default |
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(ThemeToggle, {})] }), _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" })] }));
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 (_jsxs(ThemeProvider, { attribute: "class", defaultTheme: "system", enableSystem: true, children: [_jsx(MockModeBanner, { active: mockMode }), _jsx(QueryClientProvider, { client: client, children: _jsx(AuthProvider, { children: reporting ? _jsx(ErrorReportingProvider, { options: reporting, children: children }) : children }) })] }));
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: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
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-primary underline-offset-4 hover:underline",
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-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px 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", {
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-primary underline-offset-4 hover:underline",
13
+ link: "text-link underline-offset-4 hover:underline",
14
14
  },
15
15
  size: {
16
- default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
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-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 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-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
20
- icon: "size-8",
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-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
23
- "icon-lg": "size-9",
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-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] 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-xl *:[img:last-child]:rounded-b-xl", className), ...props }));
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 rounded-t-xl 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)", 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 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 rounded-b-xl border-t bg-muted/50 p-(--card-spacing)", className), ...props }));
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-[4px] border border-input transition-colors outline-none 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, {}) }) }));
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-8 flex-wrap items-center gap-1 rounded-lg 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 }));
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/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", className), ...props }));
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-8 w-full min-w-0 items-center rounded-lg 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 }));
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: {