@calliopelabs/ui 0.3.1 → 0.5.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/README.md CHANGED
@@ -75,9 +75,9 @@ const NAV: NavModel = [
75
75
  export const astrolift = defineProduct({ ...astroliftBrand, nav: NAV });
76
76
  ```
77
77
 
78
- Astrolift AI: leaf green on the deep planet green sidebar (`#04231a`), all six grounds, graphite by default (mist when the system runs light), and its eight named themes.
78
+ Astrolift AI: leaf green on the deep planet green sidebar (`#04231a`), six grounds (every one but navy), graphite by default (mist when the system runs light), and its eight named themes.
79
79
 
80
- Zentinelle AI: teal (`#37efed`, `#0d7470` on light grounds) on the navy sidebar (`#0b0b19`), a true black ground by default, the family accents beside teal, and no emerald ground. Its seven areas gate rows by role:
80
+ Zentinelle AI: teal (`#37efed`, `#0d7470` on light grounds) on the navy sidebar (`#0b0b19`), a true black ground by default, the family accents beside teal, and no emerald or navy ground. Its seven areas gate rows by role:
81
81
 
82
82
  ```ts
83
83
  import { defineProduct, zentinelleBrand, type NavModel } from "@calliopelabs/ui";
@@ -116,6 +116,8 @@ const NAV: NavModel = [
116
116
  export const zentinelle = defineProduct({ ...zentinelleBrand, nav: NAV });
117
117
  ```
118
118
 
119
+ Calliope AI Workbench (`workbenchBrand`): Workbench blue (`#8fb8e8`, `#2f5d94` on light grounds) on a navy sidebar (`#0e1730`), its own navy ground by default (mist when the system runs light), the family accents beside its blue, and no emerald ground.
120
+
119
121
  `defineProduct` validates the config and throws on anything that would paint wrong: a colour that is not lowercase `#rrggbb`, a default ground the product does not offer, a brand accent key the family already uses.
120
122
 
121
123
  ## Wire the shell
@@ -265,7 +267,7 @@ The package fetches nothing. `DataTable` reads a `TableController`: rows, state,
265
267
 
266
268
  ## Appearance
267
269
 
268
- The person sets four axes, per product: the **ground** (black, charcoal, graphite, emerald, paper, mist; it decides light or dark), the **accent** (the product's own, a family accent, or any colour that stands 3:1 off the ground), the **density** (`compact` hairline rows or boxed `cards`) and the **corners** (0, 2, 4 or 10 px). `AppearancePicker` is the settings UI for all four.
270
+ The person sets four axes, per product: the **ground** (black, charcoal, graphite, emerald, navy, paper, mist; it decides light or dark), the **accent** (the product's own, a family accent, or any colour that stands 3:1 off the ground), the **density** (`compact` hairline rows or boxed `cards`) and the **corners** (0, 2, 4 or 10 px). `AppearancePicker` is the settings UI for all four.
269
271
 
270
272
  - Precedence: a locked org theme, then the person's choice, then the org default, then the product default. Until someone picks a ground, light or dark follows the system.
271
273
  - An org policy that arrives after sign-in goes in through `useAppearance().setPolicy`. An app that keeps the preference on its server passes `onChange` and hands the server's copy back through `applyServerAppearance`.
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { type Ground } from "../theme/tokens.js";
3
+ import { type CustomAccent } from "./appearance.js";
2
4
  /**
3
5
  * The person's appearance for the product in context: theme presets, accent
4
6
  * (including any colour that stands 3:1 off the ground), ground, density and
@@ -8,3 +10,20 @@ import * as React from "react";
8
10
  export declare function AppearancePicker({ className }: {
9
11
  className?: string;
10
12
  }): React.JSX.Element;
13
+ export interface CustomAccentFieldProps {
14
+ /** The text field's id; the help text takes `${id}-help`. */
15
+ id: string;
16
+ /** The custom accent in use, or null while a named accent is. */
17
+ value: CustomAccent | null;
18
+ /** The ground the colour must stand 3:1 off. */
19
+ ground: Ground;
20
+ disabled: boolean;
21
+ /** Called only with a colour that passes the contrast check. */
22
+ onChange: (accent: CustomAccent) => void;
23
+ }
24
+ /**
25
+ * Any colour as the accent (Astrolift palette option A). It applies only once
26
+ * it stands 3:1 off the ground (WCAG 2.2 §1.4.11); until then the draft stays
27
+ * in the field with the reason.
28
+ */
29
+ export declare function CustomAccentField({ id, value, ground, disabled, onChange, }: CustomAccentFieldProps): React.JSX.Element;
@@ -44,17 +44,24 @@ export function AppearancePicker({ className }) {
44
44
  const id = React.useId();
45
45
  const accents = product.accents;
46
46
  const activeTheme = product.themes.find((th) => th.ground === appearance.ground && th.accent === appearance.accent);
47
- return (_jsxs("div", { "data-slot": "appearance-picker", className: cn("flex min-w-0 flex-col gap-6", className), children: [locked && (_jsxs("p", { className: "flex items-center gap-2 rounded-lg border border-info-border bg-info-bg px-3 py-2 text-sm text-info-fg", children: [_jsx(LockIcon, { className: "size-4 shrink-0", "aria-hidden": true }), t.locked] })), product.themes.length > 0 && (_jsx(Group, { legend: t.themes, children: _jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2", children: product.themes.map((theme) => (_jsxs(Choice, { name: `${id}-theme`, checked: activeTheme?.id === theme.id, disabled: locked, onSelect: () => setAppearance({ ground: theme.ground, accent: theme.accent }), className: "flex-col items-stretch gap-2 p-2", children: [_jsx(ThemePreview, { product: product, ground: theme.ground, accent: accents.find((a) => a.key === theme.accent) }), _jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold", children: theme.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: theme.hint })] })] }, theme.id))) }) })), _jsxs(Group, { legend: t.accent, children: [_jsx("div", { className: "flex flex-wrap gap-2", children: accents.map((accent) => (_jsxs(Choice, { name: `${id}-accent`, checked: appearance.accent === accent.key, disabled: locked, onSelect: () => setAppearance({ accent: accent.key }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 rounded-full", style: { background: accentOn(accent, appearance.ground) } }), t.accentLabel(accent.key, accent.label)] }, accent.key))) }), _jsx(CustomAccent, { id: `${id}-custom`, value: isCustomAccent(appearance.accent) ? appearance.accent : null, ground: appearance.ground, disabled: locked, onChange: (accent) => setAppearance({ accent }) })] }), _jsx(Group, { legend: t.ground, children: _jsx("div", { className: "flex flex-wrap gap-2", children: product.grounds.map((ground) => (_jsxs(Choice, { name: `${id}-ground`, checked: appearance.ground === ground, disabled: locked, onSelect: () => setAppearance({ ground }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 rounded-full border", style: { background: SURFACES[ground].background } }), t.groundLabel(ground, GROUNDS[ground].label), _jsx("span", { className: "text-xs text-muted-foreground", children: t.mode(GROUNDS[ground].mode) })] }, ground))) }) }), _jsx(Group, { legend: t.density, children: _jsx("div", { className: "grid gap-2 sm:grid-cols-2", children: DENSITY_KEYS.map((density) => (_jsxs(Choice, { name: `${id}-density`, checked: appearance.density === density, disabled: locked, onSelect: () => setAppearance({ density }), className: "flex-col items-start gap-0.5 px-3 py-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t.densityLabel(density) }), _jsx("span", { className: "text-xs text-muted-foreground", children: t.densityHint(density) })] }, density))) }) }), _jsx(Group, { legend: t.corners, children: _jsx("div", { className: "flex flex-wrap gap-2", children: CORNERS.map((corners) => (_jsxs(Choice, { name: `${id}-corners`, checked: appearance.corners === corners, disabled: locked, onSelect: () => setAppearance({ corners }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 border-t-2 border-l-2 border-current", style: { borderTopLeftRadius: corners } }), t.cornersLabel(corners)] }, corners))) }) }), !locked && (_jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", onClick: reset, children: t.reset }) }))] }));
47
+ return (_jsxs("div", { "data-slot": "appearance-picker", className: cn("flex min-w-0 flex-col gap-6", className), children: [locked && (_jsxs("p", { className: "flex items-center gap-2 rounded-lg border border-info-border bg-info-bg px-3 py-2 text-sm text-info-fg", children: [_jsx(LockIcon, { className: "size-4 shrink-0", "aria-hidden": true }), t.locked] })), product.themes.length > 0 && (_jsx(Group, { legend: t.themes, children: _jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2", children: product.themes.map((theme) => (_jsxs(Choice, { name: `${id}-theme`, checked: activeTheme?.id === theme.id, disabled: locked, onSelect: () => setAppearance({ ground: theme.ground, accent: theme.accent }), className: "flex-col items-stretch gap-2 p-2", children: [_jsx(ThemePreview, { product: product, ground: theme.ground, accent: accents.find((a) => a.key === theme.accent) }), _jsxs("span", { children: [_jsx("span", { className: "block text-sm font-semibold", children: t.themeName(theme) }), _jsx("span", { className: "block text-xs text-muted-foreground", children: t.themeHint(theme) })] })] }, theme.id))) }) })), _jsxs(Group, { legend: t.accent, children: [_jsx("div", { className: "flex flex-wrap gap-2", children: accents.map((accent) => (_jsxs(Choice, { name: `${id}-accent`, checked: appearance.accent === accent.key, disabled: locked, onSelect: () => setAppearance({ accent: accent.key }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 rounded-full", style: { background: accentOn(accent, appearance.ground) } }), t.accentLabel(accent.key, accent.label)] }, accent.key))) }), _jsx(CustomAccentField, { id: `${id}-custom`, value: isCustomAccent(appearance.accent) ? appearance.accent : null, ground: appearance.ground, disabled: locked, onChange: (accent) => setAppearance({ accent }) })] }), _jsx(Group, { legend: t.ground, children: _jsx("div", { className: "flex flex-wrap gap-2", children: product.grounds.map((ground) => (_jsxs(Choice, { name: `${id}-ground`, checked: appearance.ground === ground, disabled: locked, onSelect: () => setAppearance({ ground }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 rounded-full border", style: { background: SURFACES[ground].background } }), t.groundLabel(ground, GROUNDS[ground].label), _jsx("span", { className: "text-xs text-muted-foreground", children: t.mode(GROUNDS[ground].mode) })] }, ground))) }) }), _jsx(Group, { legend: t.density, children: _jsx("div", { className: "grid gap-2 sm:grid-cols-2", children: DENSITY_KEYS.map((density) => (_jsxs(Choice, { name: `${id}-density`, checked: appearance.density === density, disabled: locked, onSelect: () => setAppearance({ density }), className: "flex-col items-start gap-0.5 px-3 py-2", children: [_jsx("span", { className: "text-sm font-semibold", children: t.densityLabel(density) }), _jsx("span", { className: "text-xs text-muted-foreground", children: t.densityHint(density) })] }, density))) }) }), _jsx(Group, { legend: t.corners, children: _jsx("div", { className: "flex flex-wrap gap-2", children: CORNERS.map((corners) => (_jsxs(Choice, { name: `${id}-corners`, checked: appearance.corners === corners, disabled: locked, onSelect: () => setAppearance({ corners }), children: [_jsx("span", { "aria-hidden": true, className: "size-3.5 border-t-2 border-l-2 border-current", style: { borderTopLeftRadius: corners } }), t.cornersLabel(corners)] }, corners))) }) }), !locked && (_jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", onClick: reset, children: t.reset }) }))] }));
48
48
  }
49
49
  /**
50
50
  * Any colour as the accent (Astrolift palette option A). It applies only once
51
51
  * it stands 3:1 off the ground (WCAG 2.2 §1.4.11); until then the draft stays
52
52
  * in the field with the reason.
53
53
  */
54
- function CustomAccent({ id, value, ground, disabled, onChange, }) {
54
+ export function CustomAccentField({ id, value, ground, disabled, onChange, }) {
55
55
  const { messages } = useUI();
56
56
  const t = messages.appearance;
57
57
  const [draft, setDraft] = React.useState(value ?? "");
58
+ // Controlled by `value`: a new value from outside (a server load, a reset)
59
+ // replaces the draft. Adjusted during render, not in an effect.
60
+ const [shownValue, setShownValue] = React.useState(value);
61
+ if (value !== shownValue) {
62
+ setShownValue(value);
63
+ setDraft(value ?? "");
64
+ }
58
65
  const parsed = parseCustomAccent(draft);
59
66
  const check = parsed ? accentContrast(parsed, ground) : null;
60
67
  const helpId = `${id}-help`;
@@ -94,24 +94,25 @@ export declare const zentinelleBrand: {
94
94
  };
95
95
  /**
96
96
  * Calliope AI Workbench (JunoHub, the installer's default theme, the desktop
97
- * splash): teal on navy. From junohub `custom.css` (--calliope-primary,
98
- * --calliope-dark) and the installer's workbench theme. Dark only today.
97
+ * splash): a sober light blue on navy. The navy comes from junohub `custom.css`
98
+ * (--calliope-dark); the hub's teal and violet are retired. Dark only today, so
99
+ * it defaults to its own navy ground, which no other product offers.
99
100
  */
100
101
  export declare const workbenchBrand: {
101
102
  readonly id: "workbench";
102
103
  readonly name: "Calliope AI Workbench";
103
104
  readonly accent: {
104
- readonly key: "workbench-teal";
105
- readonly label: "Workbench teal";
106
- readonly dark: "#0af0ce";
105
+ readonly key: "workbench-blue";
106
+ readonly label: "Workbench blue";
107
+ readonly dark: "#8fb8e8";
107
108
  readonly onDark: "#0b1120";
108
- readonly light: "#0b7a69";
109
+ readonly light: "#2f5d94";
109
110
  readonly onLight: "#f3fbf7";
110
111
  };
111
- readonly sidebar: "#0b1120";
112
- readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
112
+ readonly sidebar: "#0e1730";
113
+ readonly grounds: readonly ["navy", "black", "charcoal", "graphite", "paper", "mist"];
113
114
  readonly defaultGround: {
114
- readonly dark: "charcoal";
115
+ readonly dark: "navy";
115
116
  readonly light: "mist";
116
117
  };
117
118
  };
@@ -67,24 +67,25 @@ export const zentinelleBrand = {
67
67
  */
68
68
  /**
69
69
  * Calliope AI Workbench (JunoHub, the installer's default theme, the desktop
70
- * splash): teal on navy. From junohub `custom.css` (--calliope-primary,
71
- * --calliope-dark) and the installer's workbench theme. Dark only today.
70
+ * splash): a sober light blue on navy. The navy comes from junohub `custom.css`
71
+ * (--calliope-dark); the hub's teal and violet are retired. Dark only today, so
72
+ * it defaults to its own navy ground, which no other product offers.
72
73
  */
73
74
  export const workbenchBrand = {
74
75
  id: "workbench",
75
76
  name: "Calliope AI Workbench",
76
77
  accent: {
77
- key: "workbench-teal",
78
- label: "Workbench teal",
79
- dark: "#0af0ce",
78
+ key: "workbench-blue",
79
+ label: "Workbench blue",
80
+ dark: "#8fb8e8",
80
81
  onDark: "#0b1120",
81
82
  // derived: the hub has no light ground
82
- light: "#0b7a69",
83
+ light: "#2f5d94",
83
84
  onLight: "#f3fbf7",
84
85
  },
85
- sidebar: "#0b1120",
86
- grounds: ["black", "charcoal", "graphite", "paper", "mist"],
87
- defaultGround: { dark: "charcoal", light: "mist" },
86
+ sidebar: "#0e1730",
87
+ grounds: ["navy", "black", "charcoal", "graphite", "paper", "mist"],
88
+ defaultGround: { dark: "navy", light: "mist" },
88
89
  };
89
90
  /**
90
91
  * Calliope AI Chat Studio: blue, from `sql-agent-ui/src/index.css` (--brand
@@ -36,7 +36,7 @@ export interface ProductConfig {
36
36
  accent: FamilyAccent | BrandAccent;
37
37
  /** The brand surface: the sidebar on dark grounds (graphite and light grounds stay neutral). */
38
38
  sidebar: string;
39
- /** The grounds offered, in picker order. All six when absent. */
39
+ /** The grounds offered, in picker order. All seven when absent. */
40
40
  grounds?: readonly Ground[];
41
41
  /** The ground until someone chooses: one per mode, following the operating system. */
42
42
  defaultGround?: {
@@ -31,7 +31,7 @@ export function DataTable({ label, controller, columns, getRowId, empty, emptyFi
31
31
  const t = messages.table;
32
32
  const contextDensity = useDensity();
33
33
  const density = densityProp ?? contextDensity;
34
- const { rows, state, error, retry, pageSize, clearFilters } = controller;
34
+ const { rows, state, error, retry, pageSize, isFiltered, clearFilters } = controller;
35
35
  const cards = density === "cards" && Boolean(renderCard);
36
36
  const pageIds = React.useMemo(() => rows.map(getRowId), [rows, getRowId]);
37
37
  const visibleColumns = rowActions
@@ -98,7 +98,7 @@ export function DataTable({ label, controller, columns, getRowId, empty, emptyFi
98
98
  return (_jsxs("div", { className: "relative", "aria-busy": controller.isStale || undefined, children: [controller.isStale && _jsx(StaleBar, { className: "-top-1.5" }), _jsx("ul", { "aria-label": label, className: grid, children: rows.map((row) => (_jsx("li", { className: cn(box, rowClassName?.(row)), children: rowHref ? (_jsx(Link, { href: rowHref(row), className: "block h-full rounded-lg outline-none after:absolute after:inset-0 focus-visible:after:ring-2 focus-visible:after:ring-ring", children: renderCard?.(row) })) : onRowActivate ? (_jsx("button", { type: "button", "aria-label": rowLabel?.(row), onClick: () => onRowActivate(row), className: "block h-full w-full text-left outline-none after:absolute after:inset-0 focus-visible:after:ring-2 focus-visible:after:ring-ring", children: renderCard?.(row) })) : (renderCard?.(row)) }, getRowId(row)))) })] }));
99
99
  }
100
100
  })();
101
- return (_jsxs("div", { "data-slot": "data-table", "data-density": densityProp, className: cn("flex min-w-0 flex-col gap-3", className), children: [chrome === "full" && (_jsx(Toolbar, { controller: controller, placeholder: searchPlaceholder, children: toolbar })), selection && selection.selectedCount > 0 && bulkActions && !cards && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2", children: [_jsx("span", { className: "text-sm font-medium tabular-nums", children: t.selected(selection.selectedCount) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: selection.clear, children: t.clearSelection }), _jsx("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: bulkActions(selection) })] })), cards ? cardGrid : table, chrome === "full" && _jsx(Pagination, { controller: controller }), state === "loading" && (_jsx("p", { role: "status", className: "sr-only", children: t.loading }))] }));
101
+ return (_jsxs("div", { "data-slot": "data-table", "data-density": densityProp, className: cn("flex min-w-0 flex-col gap-3", className), children: [chrome === "full" && (_jsx(Toolbar, { controller: controller, placeholder: searchPlaceholder, children: toolbar })), selection && selection.selectedCount > 0 && bulkActions && !cards && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-3 py-2", children: [_jsx("span", { className: "text-sm font-medium tabular-nums", children: t.selected(selection.selectedCount) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: selection.clear, children: t.clearSelection }), _jsx("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: bulkActions(selection) })] })), cards ? cardGrid : table, chrome === "full" && _jsx(Pagination, { controller: controller }), state === "loading" && (_jsx("p", { role: "status", className: "sr-only", children: t.loading })), isFiltered && state === "ready" && (_jsx("p", { "aria-live": "polite", className: "sr-only", children: t.filteredResults }))] }));
102
102
  }
103
103
  /**
104
104
  * The rows on screen answer the previous question while the next answer is
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { accentContrast, appearanceFromServer, applyAppearance, clearAppearance,
13
13
  export { appearanceScript, productStyles } from "./appearance/boot.js";
14
14
  export { AppearanceScript, type AppearanceScriptProps } from "./appearance/AppearanceScript.js";
15
15
  export { AppearanceProvider, useAppearance, useDensity, useProduct, type AppearanceContextValue, type AppearanceProviderProps, } from "./appearance/AppearanceProvider.js";
16
- export { AppearancePicker } from "./appearance/AppearancePicker.js";
16
+ export { AppearancePicker, CustomAccentField, type CustomAccentFieldProps, } from "./appearance/AppearancePicker.js";
17
17
  export { activeFor, areaSwitcher, localizeNav, visibleNav, type CrumbSwitchOption, type NavActive, type NavArea, type NavFunction, type NavGroup, type NavIcon, type NavModel, } from "./nav/model.js";
18
18
  export { Nav, type NavProps } from "./nav/Nav.js";
19
19
  export { AppShell, type AppShellProps } from "./shell/AppShell.js";
@@ -44,7 +44,7 @@ export { ConfirmDialog, type ConfirmDialogProps, type ConfirmReason, } from "./o
44
44
  export { Button, buttonVariants, type ButtonProps } from "./primitives/Button.js";
45
45
  export { Input } from "./primitives/Input.js";
46
46
  export { Checkbox } from "./primitives/Checkbox.js";
47
- export { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./primitives/DropdownMenu.js";
48
- export { Popover, PopoverContent, PopoverTrigger } from "./primitives/Popover.js";
47
+ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./primitives/DropdownMenu.js";
48
+ export { Popover, PopoverAnchor, PopoverContent, PopoverHeader, PopoverTrigger, } from "./primitives/Popover.js";
49
49
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/Tabs.js";
50
50
  export { Skeleton } from "./primitives/Skeleton.js";
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ export { accentContrast, appearanceFromServer, applyAppearance, clearAppearance,
16
16
  export { appearanceScript, productStyles } from "./appearance/boot.js";
17
17
  export { AppearanceScript } from "./appearance/AppearanceScript.js";
18
18
  export { AppearanceProvider, useAppearance, useDensity, useProduct, } from "./appearance/AppearanceProvider.js";
19
- export { AppearancePicker } from "./appearance/AppearancePicker.js";
19
+ export { AppearancePicker, CustomAccentField, } from "./appearance/AppearancePicker.js";
20
20
  // Navigation
21
21
  export { activeFor, areaSwitcher, localizeNav, visibleNav, } from "./nav/model.js";
22
22
  export { Nav } from "./nav/Nav.js";
@@ -55,7 +55,7 @@ export { ConfirmDialog, } from "./overlays/ConfirmDialog.js";
55
55
  export { Button, buttonVariants } from "./primitives/Button.js";
56
56
  export { Input } from "./primitives/Input.js";
57
57
  export { Checkbox } from "./primitives/Checkbox.js";
58
- export { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./primitives/DropdownMenu.js";
59
- export { Popover, PopoverContent, PopoverTrigger } from "./primitives/Popover.js";
58
+ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./primitives/DropdownMenu.js";
59
+ export { Popover, PopoverAnchor, PopoverContent, PopoverHeader, PopoverTrigger, } from "./primitives/Popover.js";
60
60
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./primitives/Tabs.js";
61
61
  export { Skeleton } from "./primitives/Skeleton.js";
@@ -59,14 +59,19 @@ export interface CrumbSwitchOption {
59
59
  label: string;
60
60
  href: string;
61
61
  active?: boolean;
62
+ /** The function's key, so an app can translate the label from its catalog. */
63
+ navFunction?: string;
62
64
  }
63
65
  /**
64
66
  * The first crumb of a page inside an area: the area's name, switching
65
67
  * between its functions with the current one checked. Read from the model,
66
- * so the sidebar and the breadcrumb always agree.
68
+ * so the sidebar and the breadcrumb always agree. `navArea` and each
69
+ * option's `navFunction` carry the model's keys, so an app can translate the
70
+ * labels from its catalog.
67
71
  */
68
72
  export declare function areaSwitcher(nav: NavModel, area: string, activeFn: string): {
69
73
  label: string;
74
+ navArea: string;
70
75
  switcher: CrumbSwitchOption[];
71
76
  };
72
77
  /**
package/dist/nav/model.js CHANGED
@@ -40,13 +40,21 @@ export function activeFor(nav, pathname) {
40
40
  /**
41
41
  * The first crumb of a page inside an area: the area's name, switching
42
42
  * between its functions with the current one checked. Read from the model,
43
- * so the sidebar and the breadcrumb always agree.
43
+ * so the sidebar and the breadcrumb always agree. `navArea` and each
44
+ * option's `navFunction` carry the model's keys, so an app can translate the
45
+ * labels from its catalog.
44
46
  */
45
47
  export function areaSwitcher(nav, area, activeFn) {
46
48
  const found = nav.find((a) => a.key === area);
47
49
  return {
48
50
  label: found?.label ?? area,
49
- switcher: (found?.groups ?? []).flatMap((group) => group.functions.map((fn) => ({ label: fn.label, href: fn.href, active: fn.key === activeFn }))),
51
+ navArea: area,
52
+ switcher: (found?.groups ?? []).flatMap((group) => group.functions.map((fn) => ({
53
+ label: fn.label,
54
+ href: fn.href,
55
+ active: fn.key === activeFn,
56
+ navFunction: fn.key,
57
+ }))),
50
58
  };
51
59
  }
52
60
  /**
@@ -23,9 +23,16 @@ export interface ConfirmDialogProps {
23
23
  * Awaited on confirm, with the trimmed reason ("" without one). While it is
24
24
  * pending both buttons are disabled. Resolving `false` keeps the dialog
25
25
  * open (the caller reported the failure); any other result closes it. A
26
- * rejection keeps it open and shows the error's message in the dialog.
26
+ * rejection keeps it open and shows the error's message in the dialog,
27
+ * or hands it to `onError` when one is given.
27
28
  */
28
29
  onConfirm: (reason: string) => Promise<unknown> | unknown;
30
+ /**
31
+ * Report a rejected `onConfirm` yourself (a toast, say) instead of the
32
+ * inline error. `message` is the line the dialog would have shown. The
33
+ * dialog still stays open. A missing reason is always shown inline.
34
+ */
35
+ onError?: (error: unknown, message: string) => void;
29
36
  reason?: ConfirmReason;
30
37
  }
31
38
  /**
@@ -34,4 +41,4 @@ export interface ConfirmDialogProps {
34
41
  * place of `window.confirm`, which blocks the event loop and can say nothing
35
42
  * when the action fails.
36
43
  */
37
- export declare function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive, confirmDisabled, onConfirm, reason, }: ConfirmDialogProps): React.JSX.Element;
44
+ export declare function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive, confirmDisabled, onConfirm, onError, reason, }: ConfirmDialogProps): React.JSX.Element;
@@ -12,7 +12,7 @@ import { overlayClassName } from "./Dialog.js";
12
12
  * place of `window.confirm`, which blocks the event loop and can say nothing
13
13
  * when the action fails.
14
14
  */
15
- export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive = false, confirmDisabled = false, onConfirm, reason, }) {
15
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, cancelLabel, destructive = false, confirmDisabled = false, onConfirm, onError, reason, }) {
16
16
  const { messages } = useUI();
17
17
  const t = messages.confirm;
18
18
  const [pending, setPending] = React.useState(false);
@@ -44,11 +44,15 @@ export function ConfirmDialog({ open, onOpenChange, title, description, confirmL
44
44
  close();
45
45
  }
46
46
  catch (err) {
47
- setError(err instanceof Error && err.message
47
+ const message = err instanceof Error && err.message
48
48
  ? err.message
49
49
  : typeof err === "string" && err
50
50
  ? err
51
- : t.failed);
51
+ : t.failed;
52
+ if (onError)
53
+ onError(err, message);
54
+ else
55
+ setError(message);
52
56
  }
53
57
  finally {
54
58
  setPending(false);
@@ -62,9 +66,9 @@ export function ConfirmDialog({ open, onOpenChange, title, description, confirmL
62
66
  onOpenChange(true);
63
67
  else
64
68
  close();
65
- }, children: _jsxs(AlertDialogPrimitive.Portal, { children: [_jsx(AlertDialogPrimitive.Overlay, { "data-slot": "confirm-dialog-overlay", className: overlayClassName() }), _jsxs(AlertDialogPrimitive.Content, { "data-slot": "confirm-dialog", "aria-busy": pending || undefined, ...(description == null ? { "aria-describedby": undefined } : {}), className: "fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 grid-cols-[minmax(0,1fr)] gap-4 rounded-lg bg-popover p-5 text-popover-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-[state=open]:animate-cui-pop-in", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(AlertDialogPrimitive.Title, { className: "font-head text-base font-semibold [overflow-wrap:anywhere]", children: title }), description != null && (_jsx(AlertDialogPrimitive.Description, { className: "text-sm [overflow-wrap:anywhere] text-muted-foreground", children: description }))] }), reason && (_jsxs("div", { className: "grid min-w-0 gap-1.5", children: [_jsx("label", { htmlFor: reasonId, className: "text-sm font-medium", children: reason.label }), _jsx("textarea", { id: reasonId, value: draft, rows: 4, disabled: pending, placeholder: reason.placeholder, "aria-invalid": error != null || undefined, "aria-describedby": error ? errorId : undefined, onChange: (event) => {
69
+ }, children: _jsxs(AlertDialogPrimitive.Portal, { children: [_jsx(AlertDialogPrimitive.Overlay, { "data-slot": "confirm-dialog-overlay", className: overlayClassName("bg-black/10") }), _jsxs(AlertDialogPrimitive.Content, { "data-slot": "confirm-dialog", "aria-busy": pending || undefined, ...(description == null ? { "aria-describedby": undefined } : {}), className: "fixed top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-xs -translate-x-1/2 -translate-y-1/2 grid-cols-[minmax(0,1fr)] gap-4 rounded-xl bg-background p-4 text-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-[state=closed]:animate-cui-pop-out data-[state=open]:animate-cui-pop-in sm:max-w-sm", children: [_jsxs("div", { "data-slot": "confirm-dialog-header", className: "grid place-items-center gap-1.5 text-center sm:place-items-start sm:text-left", children: [_jsx(AlertDialogPrimitive.Title, { className: "font-head text-base font-semibold [overflow-wrap:anywhere]", children: title }), description != null && (_jsx(AlertDialogPrimitive.Description, { className: "text-sm [overflow-wrap:anywhere] text-muted-foreground", children: description }))] }), reason && (_jsxs("div", { className: "grid min-w-0 gap-1.5", children: [_jsx("label", { htmlFor: reasonId, className: "text-sm font-medium", children: reason.label }), _jsx("textarea", { id: reasonId, value: draft, rows: 4, disabled: pending, placeholder: reason.placeholder, "aria-invalid": error != null || undefined, "aria-describedby": error ? errorId : undefined, onChange: (event) => {
66
70
  setDraft(event.target.value);
67
71
  if (error)
68
72
  setError(null);
69
- }, className: "min-h-20 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50 aria-invalid:border-destructive" })] })), error && (_jsx("p", { id: errorId, role: "alert", className: "rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-sm [overflow-wrap:anywhere] text-danger-fg", children: error })), _jsxs("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", children: [_jsx(AlertDialogPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "outline", disabled: pending, children: cancelLabel ?? t.cancel }) }), _jsx(AlertDialogPrimitive.Action, { asChild: true, children: _jsx(Button, { variant: destructive ? "destructive" : "default", disabled: pending || confirmDisabled, onClick: confirm, className: cn(pending && "cursor-progress"), children: pending ? t.working : (confirmLabel ?? t.confirm) }) })] })] })] }) }));
73
+ }, className: "min-h-20 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50 aria-invalid:border-destructive" })] })), error && (_jsx("p", { id: errorId, role: "alert", className: "rounded-lg border border-danger-border bg-danger-bg px-3 py-2 text-sm [overflow-wrap:anywhere] text-danger-fg", children: error })), _jsxs("div", { "data-slot": "confirm-dialog-footer", className: "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end", children: [_jsx(AlertDialogPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "outline", disabled: pending, children: cancelLabel ?? t.cancel }) }), _jsx(AlertDialogPrimitive.Action, { asChild: true, children: _jsx(Button, { variant: destructive ? "destructive" : "default", disabled: pending || confirmDisabled, onClick: confirm, className: cn(pending && "cursor-progress"), children: pending ? t.working : (confirmLabel ?? t.confirm) }) })] })] })] }) }));
70
74
  }
@@ -3,7 +3,7 @@ import type * as React from "react";
3
3
  export declare function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element;
4
4
  export declare function DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>): React.JSX.Element;
5
5
  export declare function DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element;
6
- /** The scrim behind a modal; shared with Drawer. */
6
+ /** The scrim behind a modal; shared with Drawer and ConfirmDialog, which lighten it. */
7
7
  export declare function overlayClassName(className?: string): string;
8
8
  /**
9
9
  * A centred modal on the raised surface. Focus moves in on open and back to
@@ -13,9 +13,9 @@ export function DialogTrigger(props) {
13
13
  export function DialogClose(props) {
14
14
  return _jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
15
15
  }
16
- /** The scrim behind a modal; shared with Drawer. */
16
+ /** The scrim behind a modal; shared with Drawer and ConfirmDialog, which lighten it. */
17
17
  export function overlayClassName(className) {
18
- return cn("fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-cui-fade-in supports-backdrop-filter:backdrop-blur-xs", className);
18
+ return cn("fixed inset-0 z-50 bg-black/30 data-[state=closed]:animate-cui-fade-out data-[state=open]:animate-cui-fade-in supports-backdrop-filter:backdrop-blur-xs", className);
19
19
  }
20
20
  /**
21
21
  * A centred modal on the raised surface. Focus moves in on open and back to
@@ -23,10 +23,10 @@ export function overlayClassName(className) {
23
23
  */
24
24
  export function DialogContent({ className, children, hideCloseButton = false, ...props }) {
25
25
  const { messages } = useUI();
26
- return (_jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: overlayClassName() }), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 grid-cols-[minmax(0,1fr)] gap-4 overflow-y-auto rounded-lg bg-popover p-6 text-popover-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-[state=open]:animate-cui-pop-in", className), ...props, children: [children, !hideCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close-button", "aria-label": messages.dialog.close, className: "absolute top-3 right-3 rounded-lg p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: _jsx(XIcon, { className: "size-4", "aria-hidden": true }) }))] })] }));
26
+ return (_jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: overlayClassName() }), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 grid-cols-[minmax(0,1fr)] gap-4 overflow-y-auto rounded-xl bg-background p-6 text-foreground shadow-lg ring-1 ring-foreground/10 outline-none data-[state=closed]:animate-cui-pop-out data-[state=open]:animate-cui-pop-in sm:max-w-xl", className), ...props, children: [children, !hideCloseButton && (_jsx(DialogPrimitive.Close, { "data-slot": "dialog-close-button", "aria-label": messages.dialog.close, className: "absolute top-3 right-3 rounded-lg p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: _jsx(XIcon, { className: "size-4", "aria-hidden": true }) }))] })] }));
27
27
  }
28
28
  export function DialogHeader({ className, ...props }) {
29
- return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-1.5 pr-6", className), ...props }));
29
+ return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-1.5 pr-6 text-left", className), ...props }));
30
30
  }
31
31
  export function DialogFooter({ className, ...props }) {
32
32
  return (_jsx("div", { "data-slot": "dialog-footer", className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props }));
@@ -4,9 +4,9 @@ export declare function Drawer(props: React.ComponentProps<typeof DrawerPrimitiv
4
4
  export declare function DrawerTrigger(props: React.ComponentProps<typeof DrawerPrimitive.Trigger>): React.JSX.Element;
5
5
  export declare function DrawerClose(props: React.ComponentProps<typeof DrawerPrimitive.Close>): React.JSX.Element;
6
6
  declare const SIDE: {
7
- readonly right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=open]:animate-cui-slide-in-right sm:max-w-sm";
8
- readonly left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=open]:animate-cui-slide-in-left sm:max-w-sm";
9
- readonly bottom: "inset-x-0 bottom-0 max-h-[85dvh] border-t data-[state=open]:animate-cui-slide-in-bottom";
7
+ readonly right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:animate-cui-slide-out-right data-[state=open]:animate-cui-slide-in-right sm:max-w-sm";
8
+ readonly left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:animate-cui-slide-out-left data-[state=open]:animate-cui-slide-in-left sm:max-w-sm";
9
+ readonly bottom: "inset-x-0 bottom-0 max-h-[85dvh] border-t data-[state=closed]:animate-cui-slide-out-bottom data-[state=open]:animate-cui-slide-in-bottom";
10
10
  };
11
11
  /**
12
12
  * A modal panel from an edge: secondary content that should not cost a page
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { XIcon } from "lucide-react";
4
4
  import { Dialog as DrawerPrimitive } from "radix-ui";
5
5
  import { cn } from "../lib/cn.js";
6
+ import { Button } from "../primitives/Button.js";
6
7
  import { useUI } from "../provider/UIProvider.js";
7
8
  import { overlayClassName } from "./Dialog.js";
8
9
  export function Drawer(props) {
@@ -15,9 +16,9 @@ export function DrawerClose(props) {
15
16
  return _jsx(DrawerPrimitive.Close, { "data-slot": "drawer-close", ...props });
16
17
  }
17
18
  const SIDE = {
18
- right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=open]:animate-cui-slide-in-right sm:max-w-sm",
19
- left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=open]:animate-cui-slide-in-left sm:max-w-sm",
20
- bottom: "inset-x-0 bottom-0 max-h-[85dvh] border-t data-[state=open]:animate-cui-slide-in-bottom",
19
+ right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:animate-cui-slide-out-right data-[state=open]:animate-cui-slide-in-right sm:max-w-sm",
20
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:animate-cui-slide-out-left data-[state=open]:animate-cui-slide-in-left sm:max-w-sm",
21
+ bottom: "inset-x-0 bottom-0 max-h-[85dvh] border-t data-[state=closed]:animate-cui-slide-out-bottom data-[state=open]:animate-cui-slide-in-bottom",
21
22
  };
22
23
  /**
23
24
  * A modal panel from an edge: secondary content that should not cost a page
@@ -26,7 +27,7 @@ const SIDE = {
26
27
  */
27
28
  export function DrawerContent({ className, children, side = "right", hideCloseButton = false, ...props }) {
28
29
  const { messages } = useUI();
29
- return (_jsxs(DrawerPrimitive.Portal, { children: [_jsx(DrawerPrimitive.Overlay, { "data-slot": "drawer-overlay", className: overlayClassName() }), _jsxs(DrawerPrimitive.Content, { "data-slot": "drawer-content", "data-side": side, className: cn("fixed z-50 flex flex-col gap-4 overflow-y-auto border-border bg-background text-sm text-foreground shadow-lg outline-none", SIDE[side], className), ...props, children: [children, !hideCloseButton && (_jsx(DrawerPrimitive.Close, { "data-slot": "drawer-close-button", "aria-label": messages.dialog.close, className: "absolute top-3 right-3 rounded-lg p-1 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: _jsx(XIcon, { className: "size-4", "aria-hidden": true }) }))] })] }));
30
+ return (_jsxs(DrawerPrimitive.Portal, { children: [_jsx(DrawerPrimitive.Overlay, { "data-slot": "drawer-overlay", className: overlayClassName("bg-black/10") }), _jsxs(DrawerPrimitive.Content, { "data-slot": "drawer-content", "data-side": side, className: cn("fixed z-50 flex flex-col gap-4 overflow-y-auto border-border bg-background text-sm text-foreground shadow-lg outline-none", SIDE[side], className), ...props, children: [children, !hideCloseButton && (_jsx(DrawerPrimitive.Close, { asChild: true, children: _jsx(Button, { "data-slot": "drawer-close-button", variant: "ghost", size: "icon-sm", "aria-label": messages.dialog.close, className: "absolute top-3 right-3", children: _jsx(XIcon, { "aria-hidden": true }) }) }))] })] }));
30
31
  }
31
32
  export function DrawerHeader({ className, ...props }) {
32
33
  return (_jsx("div", { "data-slot": "drawer-header", className: cn("flex flex-col gap-1 p-4 pr-10", className), ...props }));
@@ -35,7 +36,7 @@ export function DrawerFooter({ className, ...props }) {
35
36
  return (_jsx("div", { "data-slot": "drawer-footer", className: cn("mt-auto flex flex-col gap-2 p-4", className), ...props }));
36
37
  }
37
38
  export function DrawerTitle({ className, ...props }) {
38
- return (_jsx(DrawerPrimitive.Title, { "data-slot": "drawer-title", className: cn("font-head text-base font-semibold text-foreground", className), ...props }));
39
+ return (_jsx(DrawerPrimitive.Title, { "data-slot": "drawer-title", className: cn("font-head text-base font-medium text-foreground", className), ...props }));
39
40
  }
40
41
  export function DrawerDescription({ className, ...props }) {
41
42
  return (_jsx(DrawerPrimitive.Description, { "data-slot": "drawer-description", className: cn("text-sm text-muted-foreground", className), ...props }));
@@ -18,6 +18,12 @@ export interface PageHeaderProps {
18
18
  tabs?: readonly TabNavItem[];
19
19
  /** Names the tab row. Defaults to "Page". */
20
20
  tabsLabel?: string;
21
+ /**
22
+ * Classes for the tab row, merged over its default gutter
23
+ * (`-mx-3 md:-mx-6`). Match the page's padding: a page padded `p-6` at
24
+ * every width passes `-mx-6 px-6`.
25
+ */
26
+ tabsClassName?: string;
21
27
  className?: string;
22
28
  }
23
29
  /**
@@ -25,4 +31,4 @@ export interface PageHeaderProps {
25
31
  * breadcrumb, the title row with its status, context and actions, and at
26
32
  * most one row of tabs. Pure.
27
33
  */
28
- export declare function PageHeader({ crumbs, title, status, context, description, primaryAction, menu, tabs, tabsLabel, className, }: PageHeaderProps): React.JSX.Element;
34
+ export declare function PageHeader({ crumbs, title, status, context, description, primaryAction, menu, tabs, tabsLabel, tabsClassName, className, }: PageHeaderProps): React.JSX.Element;
@@ -8,6 +8,6 @@ import { TabNav } from "./TabNav.js";
8
8
  * breadcrumb, the title row with its status, context and actions, and at
9
9
  * most one row of tabs. Pure.
10
10
  */
11
- export function PageHeader({ crumbs, title, status, context, description, primaryAction, menu, tabs, tabsLabel = "Page", className, }) {
12
- return (_jsxs("header", { "data-slot": "page-header", className: cn("flex min-w-0 flex-col gap-3", className), children: [crumbs && crumbs.length > 0 && _jsx(Breadcrumbs, { crumbs: crumbs }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2", children: [_jsx("h1", { className: "min-w-0 truncate font-head text-xl font-semibold", children: title }), status, context && (_jsx("span", { className: "min-w-0 truncate text-sm text-muted-foreground", children: context })), (primaryAction || menu) && (_jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [primaryAction, menu] }))] }), description && (_jsx("div", { className: "max-w-2xl text-sm [overflow-wrap:anywhere] text-muted-foreground", children: description })), tabs && tabs.length > 0 && (_jsx(TabNav, { tabs: tabs, label: tabsLabel, className: "-mx-3 md:-mx-6" }))] }));
11
+ export function PageHeader({ crumbs, title, status, context, description, primaryAction, menu, tabs, tabsLabel = "Page", tabsClassName, className, }) {
12
+ return (_jsxs("header", { "data-slot": "page-header", className: cn("flex min-w-0 flex-col gap-3", className), children: [crumbs && crumbs.length > 0 && _jsx(Breadcrumbs, { crumbs: crumbs }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2", children: [_jsx("h1", { className: "min-w-0 truncate font-head text-xl font-semibold", children: title }), status, context && (_jsx("span", { className: "min-w-0 truncate text-sm text-muted-foreground", children: context })), (primaryAction || menu) && (_jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [primaryAction, menu] }))] }), description && (_jsx("div", { className: "max-w-2xl text-sm [overflow-wrap:anywhere] text-muted-foreground", children: description })), tabs && tabs.length > 0 && (_jsx(TabNav, { tabs: tabs, label: tabsLabel, className: cn("-mx-3 md:-mx-6", tabsClassName) }))] }));
13
13
  }
@@ -7,13 +7,13 @@ import { cn } from "../lib/cn.js";
7
7
  * corners setting (`rounded-lg` is `--radius`), so a 0px or 10px choice
8
8
  * reaches buttons as it reaches panels.
9
9
  */
10
- export 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-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
10
+ export 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-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 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", {
11
11
  variants: {
12
12
  variant: {
13
13
  default: "bg-primary text-primary-foreground hover:bg-primary/85",
14
- outline: "border-border bg-transparent hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
14
+ 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",
15
15
  secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary",
16
- ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground",
16
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
17
17
  destructive: "bg-danger/10 text-danger-fg hover:bg-danger/15 focus-visible:border-danger-border focus-visible:ring-danger/20",
18
18
  link: "text-primary underline-offset-4 hover:underline",
19
19
  },
@@ -32,5 +32,5 @@ export const buttonVariants = cva("group/button inline-flex shrink-0 items-cente
32
32
  });
33
33
  export function Button({ className, variant, size, asChild = false, ...props }) {
34
34
  const Component = asChild ? Slot.Root : "button";
35
- return (_jsx(Component, { "data-slot": "button", "data-variant": variant ?? "default", className: cn(buttonVariants({ variant, size }), className), ...props }));
35
+ return (_jsx(Component, { "data-slot": "button", "data-variant": variant ?? "default", "data-size": size ?? "default", className: cn(buttonVariants({ variant, size }), className), ...props }));
36
36
  }
@@ -9,5 +9,5 @@ import { cn } from "../lib/cn.js";
9
9
  * state cannot be expressed in JSX on a native box.
10
10
  */
11
11
  export function Checkbox({ className, ...props }) {
12
- return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer size-4 shrink-0 rounded-sm border border-input transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "flex items-center justify-center text-current", children: props.checked === "indeterminate" ? (_jsx(MinusIcon, { className: "size-3.5", "aria-hidden": true })) : (_jsx(CheckIcon, { className: "size-3.5", "aria-hidden": true })) }) }));
12
+ return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer size-4 shrink-0 rounded-sm border border-input shadow-xs transition-shadow outline-none 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-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "flex items-center justify-center text-current transition-none", children: props.checked === "indeterminate" ? (_jsx(MinusIcon, { className: "size-3.5", "aria-hidden": true })) : (_jsx(CheckIcon, { className: "size-3.5", "aria-hidden": true })) }) }));
13
13
  }
@@ -7,5 +7,8 @@ export declare function DropdownMenuGroup(props: React.ComponentProps<typeof Dro
7
7
  export declare function DropdownMenuItem({ className, variant, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
8
8
  variant?: "default" | "destructive";
9
9
  }): React.JSX.Element;
10
+ export declare function DropdownMenuCheckboxItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>): React.JSX.Element;
11
+ export declare function DropdownMenuRadioGroup(props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>): React.JSX.Element;
12
+ export declare function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>): React.JSX.Element;
10
13
  export declare function DropdownMenuLabel({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label>): React.JSX.Element;
11
14
  export declare function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CheckIcon } from "lucide-react";
3
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
4
5
  import { cn } from "../lib/cn.js";
5
6
  export function DropdownMenu(props) {
@@ -17,6 +18,16 @@ export function DropdownMenuGroup(props) {
17
18
  export function DropdownMenuItem({ className, variant = "default", ...props }) {
18
19
  return (_jsx(DropdownMenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-variant": variant, className: cn("relative flex cursor-default items-center gap-1.5 rounded-lg px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-[variant=destructive]:text-danger-fg data-[variant=destructive]:focus:bg-danger/10 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
19
20
  }
21
+ const selectableItemClass = "relative flex cursor-default items-center gap-1.5 rounded-lg py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
22
+ export function DropdownMenuCheckboxItem({ className, children, ...props }) {
23
+ return (_jsxs(DropdownMenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", className: cn(selectableItemClass, className), ...props, children: [_jsx("span", { "data-slot": "dropdown-menu-checkbox-item-indicator", className: "pointer-events-none absolute right-2 flex items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(CheckIcon, { "aria-hidden": true }) }) }), children] }));
24
+ }
25
+ export function DropdownMenuRadioGroup(props) {
26
+ return _jsx(DropdownMenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props });
27
+ }
28
+ export function DropdownMenuRadioItem({ className, children, ...props }) {
29
+ return (_jsxs(DropdownMenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", className: cn(selectableItemClass, className), ...props, children: [_jsx("span", { "data-slot": "dropdown-menu-radio-item-indicator", className: "pointer-events-none absolute right-2 flex items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(CheckIcon, { "aria-hidden": true }) }) }), children] }));
30
+ }
20
31
  export function DropdownMenuLabel({ className, ...props }) {
21
32
  return (_jsx(DropdownMenuPrimitive.Label, { "data-slot": "dropdown-menu-label", className: cn("px-1.5 py-1 text-xs font-medium text-muted-foreground", className), ...props }));
22
33
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/cn.js";
3
3
  export function Input({ className, type, ...props }) {
4
- return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none placeholder:text-muted-foreground 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 md:text-sm", className), ...props }));
4
+ return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
5
5
  }
@@ -3,3 +3,5 @@ import type * as React from "react";
3
3
  export declare function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>): React.JSX.Element;
4
4
  export declare function PopoverTrigger(props: React.ComponentProps<typeof PopoverPrimitive.Trigger>): React.JSX.Element;
5
5
  export declare function PopoverContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>): React.JSX.Element;
6
+ export declare function PopoverAnchor(props: React.ComponentProps<typeof PopoverPrimitive.Anchor>): React.JSX.Element;
7
+ export declare function PopoverHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
@@ -11,3 +11,9 @@ export function PopoverTrigger(props) {
11
11
  export function PopoverContent({ className, align = "center", sideOffset = 4, ...props }) {
12
12
  return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: cn("z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden data-[state=open]:animate-cui-fade-in", className), ...props }) }));
13
13
  }
14
+ export function PopoverAnchor(props) {
15
+ return _jsx(PopoverPrimitive.Anchor, { "data-slot": "popover-anchor", ...props });
16
+ }
17
+ export function PopoverHeader({ className, ...props }) {
18
+ return (_jsx("div", { "data-slot": "popover-header", className: cn("flex flex-col gap-0.5 text-sm", className), ...props }));
19
+ }
@@ -1,3 +1,4 @@
1
+ import type { ProductTheme } from "../appearance/product.js";
1
2
  /**
2
3
  * Every string the package renders, in English. An app with a message
3
4
  * catalog overrides any of them through `UIProvider messages={...}`; a
@@ -34,6 +35,7 @@ export interface Messages {
34
35
  search: string;
35
36
  clearSearch: string;
36
37
  results: (count: number) => string;
38
+ filteredResults: string;
37
39
  loading: string;
38
40
  loadFailed: (label: string) => string;
39
41
  requestFailed: string;
@@ -83,6 +85,10 @@ export interface Messages {
83
85
  density: string;
84
86
  corners: string;
85
87
  reset: string;
88
+ /** A named theme's title; the default is the product's own English name. */
89
+ themeName: (theme: ProductTheme) => string;
90
+ /** A named theme's one-line hint; the default is the product's own English hint. */
91
+ themeHint: (theme: ProductTheme) => string;
86
92
  mode: (mode: "dark" | "light") => string;
87
93
  groundLabel: (ground: string, label: string) => string;
88
94
  accentLabel: (accent: string, label: string) => string;
@@ -28,6 +28,7 @@ export const defaultMessages = {
28
28
  search: "Search",
29
29
  clearSearch: "Clear search",
30
30
  results: (n) => (n === 1 ? "1 result" : `${count(n)} results`),
31
+ filteredResults: "Filtered results shown.",
31
32
  loading: "Loading",
32
33
  loadFailed: (label) => `Could not load ${lower(label)}`,
33
34
  requestFailed: "The request failed.",
@@ -76,6 +77,8 @@ export const defaultMessages = {
76
77
  density: "Density",
77
78
  corners: "Corners",
78
79
  reset: "Use the default",
80
+ themeName: (theme) => theme.name,
81
+ themeHint: (theme) => theme.hint,
79
82
  mode: (mode) => mode,
80
83
  groundLabel: (_ground, label) => label,
81
84
  accentLabel: (_accent, label) => label,
@@ -9,6 +9,8 @@ export interface RailTreeItem {
9
9
  status?: StatusTone;
10
10
  /** Read with the status dot, which carries no text of its own ("failing"). */
11
11
  statusLabel?: string;
12
+ /** In flight (pending, provisioning): the status dot pulses, unless the viewer prefers reduced motion. */
13
+ statusPulse?: boolean;
12
14
  }
13
15
  export interface RailTreeGroup {
14
16
  key: string;
@@ -27,6 +27,6 @@ function Branch({ group, activeHref }) {
27
27
  const isActive = item.href === activeHref;
28
28
  return (_jsx("li", { children: _jsxs(Link, { href: item.href, "aria-current": isActive ? "page" : undefined, className: cn("flex h-7 min-w-0 items-center gap-2 rounded-lg px-1.5 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", isActive
29
29
  ? "bg-primary/15 font-medium text-sidebar-foreground"
30
- : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60"), children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0 opacity-70", "aria-hidden": true }), _jsx("span", { className: "truncate", title: item.label, children: item.label }), " ", item.status && (_jsx(StatusDot, { tone: item.status, label: item.statusLabel, className: "ml-auto" }))] }) }, item.key));
30
+ : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60"), children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0 opacity-70", "aria-hidden": true }), _jsx("span", { className: "truncate", title: item.label, children: item.label }), " ", item.status && (_jsx(StatusDot, { tone: item.status, pulse: item.statusPulse, label: item.statusLabel, className: "ml-auto" }))] }) }, item.key));
31
31
  }) })] }));
32
32
  }
@@ -10,6 +10,6 @@ export function EmptyState({ icon, title, description, actionHref, actionLabel,
10
10
  const learnMoreText = learnMoreLabel ?? messages.states.learnMore;
11
11
  const external = learnMoreHref ? /^https?:\/\//.test(learnMoreHref) : false;
12
12
  const learnMoreClass = "mt-2 inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline";
13
- return (_jsxs("div", { "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed px-6 py-12 text-center whitespace-normal", className), children: [icon && (_jsx("div", { "aria-hidden": true, className: "rounded-lg bg-muted p-2 text-muted-foreground [&_svg]:size-5", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium [overflow-wrap:anywhere]", children: title }), description && (_jsx("p", { className: "mx-auto mt-1 max-w-sm text-sm leading-relaxed text-muted-foreground", children: description })), learnMoreHref &&
13
+ return (_jsxs("div", { "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed px-6 py-12 text-center whitespace-normal", className), children: [icon && (_jsx("div", { "aria-hidden": true, className: "rounded-lg bg-muted p-2 text-muted-foreground [&_svg:not([class*='size-'])]:size-5", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium [overflow-wrap:anywhere]", children: title }), description && (_jsx("p", { className: "mx-auto mt-1 max-w-sm text-sm leading-relaxed text-muted-foreground", children: description })), learnMoreHref &&
14
14
  (external ? (_jsxs("a", { href: learnMoreHref, target: "_blank", rel: "noreferrer", className: learnMoreClass, children: [learnMoreText, _jsx(ExternalLinkIcon, { className: "size-3", "aria-hidden": true })] })) : (_jsx(Link, { href: learnMoreHref, className: learnMoreClass, children: learnMoreText })))] }), actionHref && actionLabel && (_jsx(Button, { asChild: true, size: "sm", children: _jsx(Link, { href: actionHref, children: actionLabel }) })), secondary] }));
15
15
  }
@@ -94,12 +94,20 @@
94
94
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
95
95
  --ease-emphasized: cubic-bezier(0.3, 0, 0, 1);
96
96
 
97
- /* Overlays arrive calmly and leave at once. */
97
+ /* Overlays arrive calmly and leave quickly. */
98
98
  --animate-cui-fade-in: cui-fade-in var(--motion-fast) var(--ease-standard);
99
99
  --animate-cui-pop-in: cui-pop-in var(--motion-base) var(--ease-emphasized);
100
100
  --animate-cui-slide-in-right: cui-slide-in-right var(--motion-base) var(--ease-emphasized);
101
101
  --animate-cui-slide-in-left: cui-slide-in-left var(--motion-base) var(--ease-emphasized);
102
102
  --animate-cui-slide-in-bottom: cui-slide-in-bottom var(--motion-base) var(--ease-emphasized);
103
+ /* Exits hold their last frame so nothing flashes back before Radix unmounts. */
104
+ --animate-cui-fade-out: cui-fade-out var(--motion-fast) var(--ease-standard) forwards;
105
+ --animate-cui-pop-out: cui-pop-out var(--motion-fast) var(--ease-standard) forwards;
106
+ --animate-cui-slide-out-right: cui-slide-out-right var(--motion-fast) var(--ease-standard)
107
+ forwards;
108
+ --animate-cui-slide-out-left: cui-slide-out-left var(--motion-fast) var(--ease-standard) forwards;
109
+ --animate-cui-slide-out-bottom: cui-slide-out-bottom var(--motion-fast) var(--ease-standard)
110
+ forwards;
103
111
  /* A table's rows being replaced: a short bar travelling along the top edge. */
104
112
  --animate-cui-progress: cui-progress 1.4s var(--ease-standard) infinite;
105
113
 
@@ -141,6 +149,35 @@
141
149
  opacity: 0;
142
150
  }
143
151
  }
152
+ @keyframes cui-fade-out {
153
+ to {
154
+ opacity: 0;
155
+ }
156
+ }
157
+ @keyframes cui-pop-out {
158
+ to {
159
+ opacity: 0;
160
+ transform: scale(0.98);
161
+ }
162
+ }
163
+ @keyframes cui-slide-out-right {
164
+ to {
165
+ transform: translateX(1.5rem);
166
+ opacity: 0;
167
+ }
168
+ }
169
+ @keyframes cui-slide-out-left {
170
+ to {
171
+ transform: translateX(-1.5rem);
172
+ opacity: 0;
173
+ }
174
+ }
175
+ @keyframes cui-slide-out-bottom {
176
+ to {
177
+ transform: translateY(1.5rem);
178
+ opacity: 0;
179
+ }
180
+ }
144
181
  }
145
182
 
146
183
  @layer base {
@@ -200,6 +200,22 @@
200
200
  --sidebar-border: rgba(255, 255, 255, 0.08);
201
201
  }
202
202
 
203
+ /* Ground: Navy (dark) */
204
+ [data-mode="dark"][data-ground="navy"] {
205
+ --background: #0b1120;
206
+ --card: #152141;
207
+ --popover: #1a284a;
208
+ --secondary: #1d2b50;
209
+ --muted: #182545;
210
+ --accent: #213157;
211
+ --muted-foreground: #97a6c2;
212
+ --border: rgba(255, 255, 255, 0.1);
213
+ --input: rgba(255, 255, 255, 0.1);
214
+ --sidebar: var(--brand-sidebar, #152141);
215
+ --sidebar-accent: color-mix(in oklab, var(--primary) 16%, transparent);
216
+ --sidebar-border: rgba(255, 255, 255, 0.08);
217
+ }
218
+
203
219
  /* Ground: Paper (light) */
204
220
  [data-mode="light"][data-ground="paper"] {
205
221
  --background: #faf6ee;
@@ -32,6 +32,10 @@ export declare const GROUNDS: {
32
32
  readonly label: "Emerald";
33
33
  readonly mode: "dark";
34
34
  };
35
+ readonly navy: {
36
+ readonly label: "Navy";
37
+ readonly mode: "dark";
38
+ };
35
39
  readonly paper: {
36
40
  readonly label: "Paper";
37
41
  readonly mode: "light";
@@ -19,6 +19,7 @@ export const GROUNDS = {
19
19
  charcoal: { label: "Charcoal", mode: "dark" },
20
20
  graphite: { label: "Graphite", mode: "dark" },
21
21
  emerald: { label: "Emerald", mode: "dark" },
22
+ navy: { label: "Navy", mode: "dark" },
22
23
  paper: { label: "Paper", mode: "light" },
23
24
  mist: { label: "Mist", mode: "light" },
24
25
  };
@@ -82,6 +83,27 @@ export const SURFACES = {
82
83
  sidebarAccent: PRIMARY_TINT_16,
83
84
  sidebarBorder: "rgba(255, 255, 255, 0.08)",
84
85
  },
86
+ /*
87
+ * Workbench's navy, from JunoHub's palette: page #0b1120 and card
88
+ * rgba(22, 35, 69, 0.9) composited on the page, with the neutral hairlines
89
+ * every other dark ground uses. Its text colours are not carried over: foreground is per mode,
90
+ * and the hub's muted text (#e6e9ee) is not muted, so muted text is a
91
+ * navy-tinted grey that reads on every panel.
92
+ */
93
+ navy: {
94
+ background: "#0b1120",
95
+ card: "#152141",
96
+ popover: "#1a284a",
97
+ secondary: "#1d2b50",
98
+ muted: "#182545",
99
+ accent: "#213157",
100
+ mutedForeground: "#97a6c2",
101
+ border: "rgba(255, 255, 255, 0.1)",
102
+ input: "rgba(255, 255, 255, 0.1)",
103
+ sidebar: "brand",
104
+ sidebarAccent: PRIMARY_TINT_16,
105
+ sidebarBorder: "rgba(255, 255, 255, 0.08)",
106
+ },
85
107
  paper: {
86
108
  background: "#faf6ee",
87
109
  card: "#fffdf9",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calliopelabs/ui",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Calliope AI shared operator UI: app shell, navigation, page frame, data table and list patterns, status colours, design tokens and appearance for Astrolift AI and Zentinelle AI.",
5
5
  "keywords": [
6
6
  "calliope-ai",