@calliopelabs/ui 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +334 -2
- package/dist/appearance/AppearancePicker.d.ts +10 -0
- package/dist/appearance/AppearancePicker.js +75 -0
- package/dist/appearance/AppearanceProvider.d.ts +53 -0
- package/dist/appearance/AppearanceProvider.js +113 -0
- package/dist/appearance/AppearanceScript.d.ts +16 -0
- package/dist/appearance/AppearanceScript.js +14 -0
- package/dist/appearance/appearance.d.ts +90 -0
- package/dist/appearance/appearance.js +151 -0
- package/dist/appearance/boot.d.ts +30 -0
- package/dist/appearance/boot.js +91 -0
- package/dist/appearance/brands.d.ts +187 -0
- package/dist/appearance/brands.js +150 -0
- package/dist/appearance/product.d.ts +89 -0
- package/dist/appearance/product.js +96 -0
- package/dist/data/DataTable.d.ts +66 -0
- package/dist/data/DataTable.js +148 -0
- package/dist/data/List.d.ts +55 -0
- package/dist/data/List.js +41 -0
- package/dist/data/types.d.ts +92 -0
- package/dist/data/types.js +2 -0
- package/dist/data/use-row-selection.d.ts +17 -0
- package/dist/data/use-row-selection.js +51 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +61 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/local-store.d.ts +12 -0
- package/dist/lib/local-store.js +54 -0
- package/dist/nav/Nav.d.ts +21 -0
- package/dist/nav/Nav.js +97 -0
- package/dist/nav/model.d.ts +77 -0
- package/dist/nav/model.js +72 -0
- package/dist/overlays/ConfirmDialog.d.ts +37 -0
- package/dist/overlays/ConfirmDialog.js +70 -0
- package/dist/overlays/Dialog.d.ts +18 -0
- package/dist/overlays/Dialog.js +39 -0
- package/dist/overlays/Drawer.d.ts +24 -0
- package/dist/overlays/Drawer.js +42 -0
- package/dist/page/Breadcrumbs.d.ts +20 -0
- package/dist/page/Breadcrumbs.js +25 -0
- package/dist/page/PageFrame.d.ts +17 -0
- package/dist/page/PageFrame.js +14 -0
- package/dist/page/PageHeader.d.ts +28 -0
- package/dist/page/PageHeader.js +13 -0
- package/dist/page/Section.d.ts +18 -0
- package/dist/page/Section.js +12 -0
- package/dist/page/TabNav.d.ts +19 -0
- package/dist/page/TabNav.js +16 -0
- package/dist/primitives/Button.d.ts +16 -0
- package/dist/primitives/Button.js +36 -0
- package/dist/primitives/Checkbox.d.ts +8 -0
- package/dist/primitives/Checkbox.js +13 -0
- package/dist/primitives/DropdownMenu.d.ts +11 -0
- package/dist/primitives/DropdownMenu.js +25 -0
- package/dist/primitives/Input.d.ts +2 -0
- package/dist/primitives/Input.js +5 -0
- package/dist/primitives/Popover.d.ts +5 -0
- package/dist/primitives/Popover.js +13 -0
- package/dist/primitives/Skeleton.d.ts +3 -0
- package/dist/primitives/Skeleton.js +6 -0
- package/dist/primitives/Tabs.d.ts +12 -0
- package/dist/primitives/Tabs.js +22 -0
- package/dist/provider/UIProvider.d.ts +29 -0
- package/dist/provider/UIProvider.js +21 -0
- package/dist/provider/messages.d.ts +106 -0
- package/dist/provider/messages.js +104 -0
- package/dist/shell/AppShell.d.ts +43 -0
- package/dist/shell/AppShell.js +62 -0
- package/dist/shell/BrandMark.d.ts +14 -0
- package/dist/shell/BrandMark.js +14 -0
- package/dist/shell/RailPanel.d.ts +39 -0
- package/dist/shell/RailPanel.js +62 -0
- package/dist/shell/RailTree.d.ts +35 -0
- package/dist/shell/RailTree.js +32 -0
- package/dist/shell/Sidebar.d.ts +26 -0
- package/dist/shell/Sidebar.js +21 -0
- package/dist/shell/UserMenu.d.ts +22 -0
- package/dist/shell/UserMenu.js +31 -0
- package/dist/shell/placement.d.ts +10 -0
- package/dist/shell/placement.js +6 -0
- package/dist/shell/rail-state.d.ts +13 -0
- package/dist/shell/rail-state.js +28 -0
- package/dist/states/EmptyState.d.ts +20 -0
- package/dist/states/EmptyState.js +15 -0
- package/dist/states/ErrorState.d.ts +23 -0
- package/dist/states/ErrorState.js +16 -0
- package/dist/states/LoadingState.d.ts +14 -0
- package/dist/states/LoadingState.js +14 -0
- package/dist/status/StatusBadge.d.ts +19 -0
- package/dist/status/StatusBadge.js +13 -0
- package/dist/status/StatusDot.d.ts +14 -0
- package/dist/status/StatusDot.js +7 -0
- package/dist/status/tones.d.ts +17 -0
- package/dist/status/tones.js +29 -0
- package/dist/styles/preset.css +175 -0
- package/dist/styles/theme.css +339 -0
- package/dist/theme/contrast.d.ts +8 -0
- package/dist/theme/contrast.js +18 -0
- package/dist/theme/css.d.ts +23 -0
- package/dist/theme/css.js +116 -0
- package/dist/theme/tokens.d.ts +195 -0
- package/dist/theme/tokens.js +294 -0
- package/package.json +112 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two consoles' brands, kept here so both apps and the catalog read the
|
|
3
|
+
* same values. An app adds its navigation:
|
|
4
|
+
*
|
|
5
|
+
* export const astrolift = defineProduct({ ...astroliftBrand, nav: NAV });
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Astrolift AI: leaf green on the deep planet green sidebar. Graphite is the
|
|
9
|
+
* shipped dark ground (Astrolift #2286), with mist for people whose system
|
|
10
|
+
* runs light. The named themes are Astrolift's picker presets.
|
|
11
|
+
*/
|
|
12
|
+
export const astroliftBrand = {
|
|
13
|
+
id: "astrolift",
|
|
14
|
+
name: "Astrolift AI",
|
|
15
|
+
accent: "green",
|
|
16
|
+
sidebar: "#04231a",
|
|
17
|
+
grounds: ["black", "charcoal", "graphite", "emerald", "paper", "mist"],
|
|
18
|
+
defaultGround: { dark: "graphite", light: "mist" },
|
|
19
|
+
themes: [
|
|
20
|
+
{ id: "orbit", name: "Orbit", hint: "Black · green", ground: "black", accent: "green" },
|
|
21
|
+
{ id: "forge", name: "Forge", hint: "Black · copper", ground: "black", accent: "copper" },
|
|
22
|
+
{ id: "console", name: "Console", hint: "Charcoal · ice", ground: "charcoal", accent: "ice" },
|
|
23
|
+
{
|
|
24
|
+
id: "dusk",
|
|
25
|
+
name: "Dusk",
|
|
26
|
+
hint: "Charcoal · periwinkle",
|
|
27
|
+
ground: "charcoal",
|
|
28
|
+
accent: "periwinkle",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "graphite",
|
|
32
|
+
name: "Graphite",
|
|
33
|
+
hint: "Graphite · green",
|
|
34
|
+
ground: "graphite",
|
|
35
|
+
accent: "green",
|
|
36
|
+
},
|
|
37
|
+
{ id: "field", name: "Field", hint: "Emerald · green", ground: "emerald", accent: "green" },
|
|
38
|
+
{ id: "daylight", name: "Daylight", hint: "Mist · green", ground: "mist", accent: "green" },
|
|
39
|
+
{ id: "workshop", name: "Workshop", hint: "Paper · copper", ground: "paper", accent: "copper" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Zentinelle AI: teal on the navy sidebar, on a true black ground
|
|
44
|
+
* (ui-overhaul-ia §5). The family accents stay selectable beside teal. The
|
|
45
|
+
* emerald ground is Astrolift's planet green and is not offered.
|
|
46
|
+
*/
|
|
47
|
+
export const zentinelleBrand = {
|
|
48
|
+
id: "zentinelle",
|
|
49
|
+
name: "Zentinelle AI",
|
|
50
|
+
accent: {
|
|
51
|
+
key: "teal",
|
|
52
|
+
label: "Teal",
|
|
53
|
+
dark: "#37efed",
|
|
54
|
+
onDark: "#0b0b19",
|
|
55
|
+
light: "#0d7470",
|
|
56
|
+
onLight: "#f3fbf7",
|
|
57
|
+
},
|
|
58
|
+
sidebar: "#0b0b19",
|
|
59
|
+
grounds: ["black", "charcoal", "graphite", "paper", "mist"],
|
|
60
|
+
defaultGround: { dark: "black", light: "mist" },
|
|
61
|
+
};
|
|
62
|
+
/*
|
|
63
|
+
* The brands below are copied from what each product ships today, so nothing
|
|
64
|
+
* changes when an app moves onto the library. Where a product has no value
|
|
65
|
+
* the model needs (usually a light-ground accent), the nearest one that
|
|
66
|
+
* passes the contrast tests is used and marked "derived" for the designer.
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
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.
|
|
72
|
+
*/
|
|
73
|
+
export const workbenchBrand = {
|
|
74
|
+
id: "workbench",
|
|
75
|
+
name: "Calliope AI Workbench",
|
|
76
|
+
accent: {
|
|
77
|
+
key: "workbench-teal",
|
|
78
|
+
label: "Workbench teal",
|
|
79
|
+
dark: "#0af0ce",
|
|
80
|
+
onDark: "#0b1120",
|
|
81
|
+
// derived: the hub has no light ground
|
|
82
|
+
light: "#0b7a69",
|
|
83
|
+
onLight: "#f3fbf7",
|
|
84
|
+
},
|
|
85
|
+
sidebar: "#0b1120",
|
|
86
|
+
grounds: ["black", "charcoal", "graphite", "paper", "mist"],
|
|
87
|
+
defaultGround: { dark: "charcoal", light: "mist" },
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Calliope AI Chat Studio: blue, from `sql-agent-ui/src/index.css` (--brand
|
|
91
|
+
* in :root and .dark, --sidebar-background in .dark).
|
|
92
|
+
*/
|
|
93
|
+
export const chatStudioBrand = {
|
|
94
|
+
id: "chat-studio",
|
|
95
|
+
name: "Calliope AI Chat Studio",
|
|
96
|
+
accent: {
|
|
97
|
+
key: "studio-blue",
|
|
98
|
+
label: "Studio blue",
|
|
99
|
+
dark: "#8ab4f8",
|
|
100
|
+
onDark: "#0c0c0c",
|
|
101
|
+
light: "#1d4ed8",
|
|
102
|
+
onLight: "#ffffff",
|
|
103
|
+
},
|
|
104
|
+
sidebar: "#080808",
|
|
105
|
+
grounds: ["black", "charcoal", "graphite", "paper", "mist"],
|
|
106
|
+
defaultGround: { dark: "black", light: "paper" },
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Calliope AI Loadr: Catppuccin blue, from `frontend/src/index.css`
|
|
110
|
+
* (--ctp-blue for Latte and Mocha, --accent-strong-fg). Sidebar is Mocha's
|
|
111
|
+
* mantle. Dark by default.
|
|
112
|
+
*/
|
|
113
|
+
export const loadrBrand = {
|
|
114
|
+
id: "loadr",
|
|
115
|
+
name: "Calliope AI Loadr",
|
|
116
|
+
accent: {
|
|
117
|
+
key: "loadr-blue",
|
|
118
|
+
label: "Loadr blue",
|
|
119
|
+
dark: "#89b4fa",
|
|
120
|
+
onDark: "#11111b",
|
|
121
|
+
light: "#1e66f5",
|
|
122
|
+
onLight: "#ffffff",
|
|
123
|
+
},
|
|
124
|
+
sidebar: "#181825",
|
|
125
|
+
grounds: ["black", "charcoal", "graphite", "paper", "mist"],
|
|
126
|
+
defaultGround: { dark: "charcoal", light: "paper" },
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Cykick: cyan on near-black with square corners, from
|
|
130
|
+
* `ui/theme/native-themes.json` (primary, background, radius 0). Cykick's
|
|
131
|
+
* own plugins read tokens, not these components; the brand keeps the family
|
|
132
|
+
* values in one place.
|
|
133
|
+
*/
|
|
134
|
+
export const cykickBrand = {
|
|
135
|
+
id: "cykick",
|
|
136
|
+
name: "Cykick",
|
|
137
|
+
accent: {
|
|
138
|
+
key: "cykick-cyan",
|
|
139
|
+
label: "Cykick cyan",
|
|
140
|
+
dark: "#22d3ee",
|
|
141
|
+
onDark: "#07090e",
|
|
142
|
+
// derived: no light accent in native-themes.json
|
|
143
|
+
light: "#0e7490",
|
|
144
|
+
onLight: "#ffffff",
|
|
145
|
+
},
|
|
146
|
+
sidebar: "#07090e",
|
|
147
|
+
grounds: ["black", "charcoal", "graphite", "paper", "mist"],
|
|
148
|
+
defaultGround: { dark: "black", light: "mist" },
|
|
149
|
+
defaultCorners: 0,
|
|
150
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { NavModel } from "../nav/model.js";
|
|
2
|
+
import { type Density, type FamilyAccent, type Ground, type Mode } from "../theme/tokens.js";
|
|
3
|
+
export type Corners = 0 | 2 | 4 | 10;
|
|
4
|
+
export declare const CORNERS: readonly Corners[];
|
|
5
|
+
/** A product's own accent, offered first in the picker (Zentinelle teal). */
|
|
6
|
+
export interface BrandAccent {
|
|
7
|
+
/** Lowercase slug, stored in the preference and set as `data-accent`. */
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** The accent on dark grounds, and the ink on it. */
|
|
11
|
+
dark: string;
|
|
12
|
+
onDark: string;
|
|
13
|
+
/** The accent on light grounds, and the ink on it. */
|
|
14
|
+
light: string;
|
|
15
|
+
onLight: string;
|
|
16
|
+
}
|
|
17
|
+
/** A named ground and accent, so the picker offers a decision rather than two lists. */
|
|
18
|
+
export interface ProductTheme {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
hint: string;
|
|
22
|
+
ground: Ground;
|
|
23
|
+
accent: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Everything that makes one console look like itself: its name, its accent,
|
|
27
|
+
* its sidebar and its navigation. The family palette, the status colours and
|
|
28
|
+
* the layout are shared.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProductConfig {
|
|
31
|
+
/** Lowercase slug: the storage namespace and `data-product` on <html>. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The full product name, as the brand mark shows it ("Astrolift AI"). */
|
|
34
|
+
name: string;
|
|
35
|
+
/** The default accent: a family accent, or the product's own. */
|
|
36
|
+
accent: FamilyAccent | BrandAccent;
|
|
37
|
+
/** The brand surface: the sidebar on dark grounds (graphite and light grounds stay neutral). */
|
|
38
|
+
sidebar: string;
|
|
39
|
+
/** The grounds offered, in picker order. All six when absent. */
|
|
40
|
+
grounds?: readonly Ground[];
|
|
41
|
+
/** The ground until someone chooses: one per mode, following the operating system. */
|
|
42
|
+
defaultGround?: {
|
|
43
|
+
dark: Ground;
|
|
44
|
+
light: Ground;
|
|
45
|
+
};
|
|
46
|
+
defaultDensity?: Density;
|
|
47
|
+
defaultCorners?: Corners;
|
|
48
|
+
themes?: readonly ProductTheme[];
|
|
49
|
+
/** The navigation model, before it is filtered by what the viewer may open. */
|
|
50
|
+
nav?: NavModel;
|
|
51
|
+
}
|
|
52
|
+
export interface ProductAccent {
|
|
53
|
+
key: string;
|
|
54
|
+
label: string;
|
|
55
|
+
dark: string;
|
|
56
|
+
onDark: string;
|
|
57
|
+
light: string;
|
|
58
|
+
onLight: string;
|
|
59
|
+
/** True for the product's own accent. */
|
|
60
|
+
brand: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** A validated product config with its defaults filled in. */
|
|
63
|
+
export interface Product {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
sidebar: string;
|
|
67
|
+
grounds: readonly Ground[];
|
|
68
|
+
/** The product's accent first, then the family accents. */
|
|
69
|
+
accents: readonly ProductAccent[];
|
|
70
|
+
/** The product's own accent when it has one; null when it uses a family accent. */
|
|
71
|
+
brandAccent: BrandAccent | null;
|
|
72
|
+
defaults: {
|
|
73
|
+
ground: Record<Mode, Ground>;
|
|
74
|
+
accent: string;
|
|
75
|
+
density: Density;
|
|
76
|
+
corners: Corners;
|
|
77
|
+
};
|
|
78
|
+
themes: readonly ProductTheme[];
|
|
79
|
+
nav: NavModel;
|
|
80
|
+
/** localStorage key of the personal appearance preference. */
|
|
81
|
+
storageKey: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate a product config and fill in its defaults. Throws on anything
|
|
85
|
+
* that would paint wrong or inject into the generated CSS: a key that is not
|
|
86
|
+
* a slug, a colour that is not lowercase `#rrggbb`, a default ground the
|
|
87
|
+
* product does not offer, or a default ground of the wrong mode.
|
|
88
|
+
*/
|
|
89
|
+
export declare function defineProduct(config: ProductConfig): Product;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ACCENTS, ACCENT_KEYS, GROUNDS, GROUND_KEYS, } from "../theme/tokens.js";
|
|
2
|
+
export const CORNERS = [0, 2, 4, 10];
|
|
3
|
+
const SLUG = /^[a-z][a-z0-9-]*$/;
|
|
4
|
+
const HEX = /^#[0-9a-f]{6}$/;
|
|
5
|
+
function fail(product, message) {
|
|
6
|
+
throw new Error(`@calliopelabs/ui: product "${product}": ${message}`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validate a product config and fill in its defaults. Throws on anything
|
|
10
|
+
* that would paint wrong or inject into the generated CSS: a key that is not
|
|
11
|
+
* a slug, a colour that is not lowercase `#rrggbb`, a default ground the
|
|
12
|
+
* product does not offer, or a default ground of the wrong mode.
|
|
13
|
+
*/
|
|
14
|
+
export function defineProduct(config) {
|
|
15
|
+
const { id } = config;
|
|
16
|
+
if (!SLUG.test(id))
|
|
17
|
+
fail(id, "id must be a lowercase slug");
|
|
18
|
+
if (!config.name.trim())
|
|
19
|
+
fail(id, "name is required");
|
|
20
|
+
if (!HEX.test(config.sidebar))
|
|
21
|
+
fail(id, `sidebar must be lowercase #rrggbb, got ${config.sidebar}`);
|
|
22
|
+
const grounds = config.grounds ? [...config.grounds] : [...GROUND_KEYS];
|
|
23
|
+
if (grounds.length === 0)
|
|
24
|
+
fail(id, "offer at least one ground");
|
|
25
|
+
for (const ground of grounds) {
|
|
26
|
+
if (!(ground in GROUNDS))
|
|
27
|
+
fail(id, `unknown ground ${ground}`);
|
|
28
|
+
}
|
|
29
|
+
const defaultGround = config.defaultGround ?? {
|
|
30
|
+
dark: grounds.find((g) => GROUNDS[g].mode === "dark") ?? fail(id, "offer a dark ground"),
|
|
31
|
+
light: grounds.find((g) => GROUNDS[g].mode === "light") ?? fail(id, "offer a light ground"),
|
|
32
|
+
};
|
|
33
|
+
for (const mode of ["dark", "light"]) {
|
|
34
|
+
const ground = defaultGround[mode];
|
|
35
|
+
if (!grounds.includes(ground))
|
|
36
|
+
fail(id, `default ${mode} ground ${ground} is not offered`);
|
|
37
|
+
if (GROUNDS[ground].mode !== mode)
|
|
38
|
+
fail(id, `default ${mode} ground ${ground} is a ${GROUNDS[ground].mode} ground`);
|
|
39
|
+
}
|
|
40
|
+
const family = ACCENT_KEYS.map((key) => ({
|
|
41
|
+
key,
|
|
42
|
+
...ACCENTS[key],
|
|
43
|
+
brand: false,
|
|
44
|
+
}));
|
|
45
|
+
let brandAccent = null;
|
|
46
|
+
if (typeof config.accent === "string") {
|
|
47
|
+
if (!(config.accent in ACCENTS))
|
|
48
|
+
fail(id, `unknown accent ${config.accent}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
brandAccent = config.accent;
|
|
52
|
+
if (!SLUG.test(brandAccent.key))
|
|
53
|
+
fail(id, "brand accent key must be a lowercase slug");
|
|
54
|
+
if (brandAccent.key in ACCENTS || brandAccent.key === "custom") {
|
|
55
|
+
fail(id, `brand accent key ${brandAccent.key} is taken by the family`);
|
|
56
|
+
}
|
|
57
|
+
for (const field of ["dark", "onDark", "light", "onLight"]) {
|
|
58
|
+
if (!HEX.test(brandAccent[field]))
|
|
59
|
+
fail(id, `brand accent ${field} must be lowercase #rrggbb`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const accents = brandAccent
|
|
63
|
+
? [{ ...brandAccent, brand: true }, ...family]
|
|
64
|
+
: family;
|
|
65
|
+
const defaultAccent = typeof config.accent === "string" ? config.accent : config.accent.key;
|
|
66
|
+
const density = config.defaultDensity ?? "compact";
|
|
67
|
+
if (density !== "compact" && density !== "cards")
|
|
68
|
+
fail(id, `unknown density ${String(density)}`);
|
|
69
|
+
const corners = config.defaultCorners ?? 2;
|
|
70
|
+
if (!CORNERS.includes(corners))
|
|
71
|
+
fail(id, `corners must be one of ${CORNERS.join(", ")}`);
|
|
72
|
+
const themes = config.themes ?? [];
|
|
73
|
+
for (const theme of themes) {
|
|
74
|
+
if (!grounds.includes(theme.ground))
|
|
75
|
+
fail(id, `theme ${theme.id} uses ground ${theme.ground}, which is not offered`);
|
|
76
|
+
if (!accents.some((a) => a.key === theme.accent))
|
|
77
|
+
fail(id, `theme ${theme.id} uses unknown accent ${theme.accent}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
name: config.name,
|
|
82
|
+
sidebar: config.sidebar,
|
|
83
|
+
grounds,
|
|
84
|
+
accents,
|
|
85
|
+
brandAccent,
|
|
86
|
+
defaults: {
|
|
87
|
+
ground: defaultGround,
|
|
88
|
+
accent: defaultAccent,
|
|
89
|
+
density,
|
|
90
|
+
corners,
|
|
91
|
+
},
|
|
92
|
+
themes,
|
|
93
|
+
nav: config.nav ?? [],
|
|
94
|
+
storageKey: `${id}.appearance`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Density } from "../theme/tokens.js";
|
|
3
|
+
import { type Column, type EmptyStateSpec, type TableController } from "./types.js";
|
|
4
|
+
import type { RowSelection } from "./use-row-selection.js";
|
|
5
|
+
type RowActivation<TRow> = {
|
|
6
|
+
/** Rows that navigate are real links: middle-click, open in a new tab and copy link all work. */
|
|
7
|
+
rowHref: (row: TRow) => string;
|
|
8
|
+
onRowActivate?: never;
|
|
9
|
+
/** Names a row for its checkbox and its menu ("run 7f3c2a91"). Defaults to the row id. */
|
|
10
|
+
rowLabel?: (row: TRow) => string;
|
|
11
|
+
} | {
|
|
12
|
+
/** Rows that act in place are buttons, never fake links. */
|
|
13
|
+
onRowActivate: (row: TRow) => void;
|
|
14
|
+
/**
|
|
15
|
+
* The activator's accessible name, and the name of the row's checkbox
|
|
16
|
+
* and menu. Identify the row ("run 7f3c2a91"); the role supplies the verb.
|
|
17
|
+
*/
|
|
18
|
+
rowLabel: (row: TRow) => string;
|
|
19
|
+
rowHref?: never;
|
|
20
|
+
} | {
|
|
21
|
+
rowHref?: never;
|
|
22
|
+
onRowActivate?: never;
|
|
23
|
+
rowLabel?: (row: TRow) => string;
|
|
24
|
+
};
|
|
25
|
+
interface DataTableBaseProps<TRow> {
|
|
26
|
+
/** Names the table for screen readers and in the state copy ("Could not load runs"). */
|
|
27
|
+
label: string;
|
|
28
|
+
controller: TableController<TRow>;
|
|
29
|
+
columns: readonly Column<TRow>[];
|
|
30
|
+
getRowId: (row: TRow) => string;
|
|
31
|
+
/** The list is genuinely empty. Required: a table never renders nothing. */
|
|
32
|
+
empty: EmptyStateSpec;
|
|
33
|
+
/** The search or filters excluded everything. Defaults to a clear-search prompt. */
|
|
34
|
+
emptyFiltered?: Pick<EmptyStateSpec, "title" | "description">;
|
|
35
|
+
/** Turns on the row and page checkboxes. */
|
|
36
|
+
selection?: RowSelection;
|
|
37
|
+
/** Shown in a bar above the table while rows are selected. */
|
|
38
|
+
bulkActions?: (selection: RowSelection) => React.ReactNode;
|
|
39
|
+
/** Items for each row's `⋯` menu (`DropdownMenuItem`s). */
|
|
40
|
+
rowActions?: (row: TRow) => React.ReactNode;
|
|
41
|
+
searchPlaceholder?: string;
|
|
42
|
+
/** Filters and actions beside the search box. */
|
|
43
|
+
toolbar?: React.ReactNode;
|
|
44
|
+
rowClassName?: (row: TRow) => string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The card for a row, drawn when the density is `cards`: the content only;
|
|
47
|
+
* the table draws the box. Without it, cards density keeps the table with
|
|
48
|
+
* roomier rows. Cards carry no checkboxes and no row menu.
|
|
49
|
+
*/
|
|
50
|
+
renderCard?: (row: TRow) => React.ReactNode;
|
|
51
|
+
/** Overrides the person's density for this table. */
|
|
52
|
+
density?: Density;
|
|
53
|
+
/** `table` draws the table alone, for a page that draws its own filter bar and paging. */
|
|
54
|
+
chrome?: "full" | "table";
|
|
55
|
+
className?: string;
|
|
56
|
+
}
|
|
57
|
+
export type DataTableProps<TRow> = DataTableBaseProps<TRow> & RowActivation<TRow>;
|
|
58
|
+
/**
|
|
59
|
+
* The house list surface (Astrolift #1231), with its guarantees in the
|
|
60
|
+
* types: `label` and `empty` are required, `empty` and `emptyFiltered` are
|
|
61
|
+
* different states, rows are links or buttons and never both, loading draws
|
|
62
|
+
* skeleton rows inside the real table, and sorting is the server's. Density
|
|
63
|
+
* is real: compact rows, or boxed cards when the table has a card renderer.
|
|
64
|
+
*/
|
|
65
|
+
export declare function DataTable<TRow>({ label, controller, columns, getRowId, empty, emptyFiltered, selection, bulkActions, rowActions, searchPlaceholder, toolbar, rowClassName, renderCard, density: densityProp, chrome, className, rowHref, onRowActivate, rowLabel, }: DataTableProps<TRow>): React.JSX.Element;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ArrowDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon, MoreHorizontalIcon, SearchIcon, SearchXIcon, TriangleAlertIcon, XIcon, } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { useDensity } from "../appearance/AppearanceProvider.js";
|
|
6
|
+
import { cn } from "../lib/cn.js";
|
|
7
|
+
import { Button } from "../primitives/Button.js";
|
|
8
|
+
import { Checkbox } from "../primitives/Checkbox.js";
|
|
9
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "../primitives/DropdownMenu.js";
|
|
10
|
+
import { Input } from "../primitives/Input.js";
|
|
11
|
+
import { Skeleton } from "../primitives/Skeleton.js";
|
|
12
|
+
import { useUI } from "../provider/UIProvider.js";
|
|
13
|
+
import { EmptyState } from "../states/EmptyState.js";
|
|
14
|
+
import { PAGE_SIZES, } from "./types.js";
|
|
15
|
+
const ALIGN = { left: "text-left", right: "text-right", center: "text-center" };
|
|
16
|
+
const CELL = "px-(--row-pad-x) py-(--row-pad-y) align-middle whitespace-nowrap";
|
|
17
|
+
function sortKeys(sort) {
|
|
18
|
+
if (!sort)
|
|
19
|
+
return [];
|
|
20
|
+
return Array.isArray(sort) ? sort : [sort];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The house list surface (Astrolift #1231), with its guarantees in the
|
|
24
|
+
* types: `label` and `empty` are required, `empty` and `emptyFiltered` are
|
|
25
|
+
* different states, rows are links or buttons and never both, loading draws
|
|
26
|
+
* skeleton rows inside the real table, and sorting is the server's. Density
|
|
27
|
+
* is real: compact rows, or boxed cards when the table has a card renderer.
|
|
28
|
+
*/
|
|
29
|
+
export function DataTable({ label, controller, columns, getRowId, empty, emptyFiltered, selection, bulkActions, rowActions, searchPlaceholder, toolbar, rowClassName, renderCard, density: densityProp, chrome = "full", className, rowHref, onRowActivate, rowLabel, }) {
|
|
30
|
+
const { Link, messages } = useUI();
|
|
31
|
+
const t = messages.table;
|
|
32
|
+
const contextDensity = useDensity();
|
|
33
|
+
const density = densityProp ?? contextDensity;
|
|
34
|
+
const { rows, state, error, retry, pageSize, clearFilters } = controller;
|
|
35
|
+
const cards = density === "cards" && Boolean(renderCard);
|
|
36
|
+
const pageIds = React.useMemo(() => rows.map(getRowId), [rows, getRowId]);
|
|
37
|
+
const visibleColumns = rowActions
|
|
38
|
+
? [
|
|
39
|
+
...columns,
|
|
40
|
+
{
|
|
41
|
+
id: "__actions",
|
|
42
|
+
header: _jsx("span", { className: "sr-only", children: t.actions }),
|
|
43
|
+
width: "w-10",
|
|
44
|
+
align: "right",
|
|
45
|
+
// Above the row's stretched link, so the menu opens instead of the row.
|
|
46
|
+
cellClassName: "relative z-10",
|
|
47
|
+
cell: (row) => (_jsx(RowMenu, { label: t.rowActions(rowLabel?.(row) ?? getRowId(row)), children: rowActions(row) })),
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
: [...columns];
|
|
51
|
+
const columnCount = visibleColumns.length + (selection ? 1 : 0);
|
|
52
|
+
const firstColumnId = visibleColumns[0]?.id;
|
|
53
|
+
const sorts = sortKeys(controller.sort);
|
|
54
|
+
const message = (icon, title, body, action, alert = false) => (_jsxs("div", { role: alert ? "alert" : undefined, className: "flex flex-col items-center gap-3 px-6 text-center", children: [icon, _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: title }), _jsx("p", { className: "mt-1 max-w-md text-sm [overflow-wrap:anywhere] text-muted-foreground", children: body })] }), action] }));
|
|
55
|
+
const errorMessage = message(_jsx(TriangleAlertIcon, { className: "size-5 text-danger", "aria-hidden": true }), t.loadFailed(label), error?.message || t.requestFailed, _jsx(Button, { size: "sm", variant: "outline", onClick: retry, children: t.retry }), true);
|
|
56
|
+
const filteredMessage = message(_jsx(SearchXIcon, { className: "size-5 text-muted-foreground", "aria-hidden": true }), emptyFiltered?.title ?? t.noMatches, emptyFiltered?.description ?? t.noMatchesDescription(label), _jsx(Button, { size: "sm", variant: "outline", onClick: clearFilters, children: t.clearSearch }));
|
|
57
|
+
// A message spanning the row stays in view when a wide table scrolls
|
|
58
|
+
// sideways: it is as wide as the scroll container (100cqw), pinned left.
|
|
59
|
+
const fullRow = (content, padded = true) => (_jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "p-0", children: _jsx("div", { className: cn("sticky left-0 w-[100cqw] max-w-full", padded && "py-10"), children: content }) }) }));
|
|
60
|
+
const tableBody = (() => {
|
|
61
|
+
switch (state) {
|
|
62
|
+
case "loading":
|
|
63
|
+
// Skeleton rows inside the real table, sized from the columns, so the
|
|
64
|
+
// first paint has the geometry the rows will have.
|
|
65
|
+
return Array.from({ length: Math.min(pageSize, 8) }, (_, i) => (_jsxs("tr", { className: "border-b last:border-0", children: [selection && (_jsx("td", { className: cn(CELL, "w-10"), children: _jsx(Skeleton, { className: "size-4" }) })), visibleColumns.map((column) => (_jsx("td", { className: cn(CELL, column.width), children: _jsx(Skeleton, { className: "h-4 w-full" }) }, column.id)))] }, `skeleton-${i}`)));
|
|
66
|
+
case "error":
|
|
67
|
+
return fullRow(errorMessage);
|
|
68
|
+
case "emptyFiltered":
|
|
69
|
+
return fullRow(filteredMessage);
|
|
70
|
+
case "empty":
|
|
71
|
+
return fullRow(_jsx(EmptyState, { ...empty, className: "rounded-none border-0" }), false);
|
|
72
|
+
case "ready":
|
|
73
|
+
return rows.map((row) => {
|
|
74
|
+
const id = getRowId(row);
|
|
75
|
+
const selected = selection?.isSelected(id) ?? false;
|
|
76
|
+
return (_jsxs("tr", { "data-state": selected ? "selected" : undefined,
|
|
77
|
+
// `relative`: the activator is stretched with `after:inset-0`,
|
|
78
|
+
// which reaches the nearest positioned ancestor; without it the
|
|
79
|
+
// last row's overlay would swallow every click (Astrolift #1502).
|
|
80
|
+
className: cn("border-b transition-colors last:border-0 hover:bg-muted/50 data-[state=selected]:bg-muted", (rowHref || onRowActivate) && "relative cursor-pointer", rowClassName?.(row)), children: [selection && (_jsx("td", { className: cn(CELL, "relative z-10 w-10"), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => selection.toggle(id), "aria-label": t.selectRow(rowLabel?.(row) ?? id) }) })), visibleColumns.map((column) => (_jsx("td", { className: cn(CELL, column.width, column.align && ALIGN[column.align], column.cellClassName), children: column.id === firstColumnId && rowHref ? (_jsx(Link, { href: rowHref(row), className: "outline-none after:absolute after:inset-0 focus-visible:after:ring-2 focus-visible:after:ring-ring focus-visible:after:ring-inset", children: column.cell(row) })) : column.id === firstColumnId && onRowActivate ? (_jsx("button", { type: "button", "aria-label": rowLabel?.(row), onClick: () => onRowActivate(row), className: "text-left outline-none after:absolute after:inset-0 focus-visible:after:ring-2 focus-visible:after:ring-ring focus-visible:after:ring-inset", children: column.cell(row) })) : (column.cell(row)) }, column.id)))] }, id));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
const table = (_jsxs("div", { className: "relative overflow-hidden rounded-lg border", "aria-busy": state === "loading" || controller.isStale || undefined, children: [controller.isStale && _jsx(StaleBar, {}), _jsx("div", { className: "[container-type:inline-size] relative w-full overflow-x-auto", children: _jsxs("table", { className: "w-full caption-bottom text-sm", children: [_jsx("caption", { className: "sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { className: "border-b", children: [selection && (_jsx("th", { scope: "col", className: "h-10 w-10 px-(--row-pad-x) align-middle", children: _jsx(Checkbox, { checked: selection.pageSelectionState(pageIds), onCheckedChange: () => selection.togglePage(pageIds), "aria-label": t.selectPage(label), disabled: pageIds.length === 0 }) })), visibleColumns.map((column) => (_jsx("th", { scope: "col", "aria-sort": ariaSort(column.sortKey, sorts), className: cn("h-10 px-(--row-pad-x) text-left align-middle text-2xs font-semibold tracking-[0.08em] whitespace-nowrap text-muted-foreground uppercase", column.width, column.align && ALIGN[column.align], column.headClassName), children: column.sortKey && controller.sortEnabled ? (_jsx(SortHeader, { sortKey: column.sortKey, sorts: sorts, onToggle: controller.toggleSort, children: column.header })) : (column.header) }, column.id)))] }) }), _jsx("tbody", { children: tableBody })] }) })] }));
|
|
85
|
+
const cardGrid = (() => {
|
|
86
|
+
const grid = "grid min-w-0 grid-cols-(--list-columns) gap-(--list-gap)";
|
|
87
|
+
const box = "relative min-w-0 rounded-(--list-item-radius) border-(length:--list-item-border) bg-(--list-item-bg) [padding:var(--list-item-pad)]";
|
|
88
|
+
switch (state) {
|
|
89
|
+
case "loading":
|
|
90
|
+
return (_jsx("div", { className: grid, "aria-busy": true, children: Array.from({ length: Math.min(pageSize, 6) }, (_, i) => (_jsx(Skeleton, { className: "h-28 w-full" }, i))) }));
|
|
91
|
+
case "error":
|
|
92
|
+
return _jsx("div", { className: "rounded-lg border py-10", children: errorMessage });
|
|
93
|
+
case "emptyFiltered":
|
|
94
|
+
return _jsx("div", { className: "rounded-lg border py-10", children: filteredMessage });
|
|
95
|
+
case "empty":
|
|
96
|
+
return _jsx(EmptyState, { ...empty });
|
|
97
|
+
case "ready":
|
|
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
|
+
}
|
|
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 }))] }));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The rows on screen answer the previous question while the next answer is
|
|
105
|
+
* on its way. They stay at full contrast; this line across the top of the
|
|
106
|
+
* frame says they are being replaced. With reduced motion it holds still.
|
|
107
|
+
*/
|
|
108
|
+
function StaleBar({ className }) {
|
|
109
|
+
return (_jsx("div", { "aria-hidden": true, className: cn("pointer-events-none absolute inset-x-0 top-0 z-20 h-0.5 overflow-hidden", className), children: _jsx("div", { className: "h-full w-1/3 animate-cui-progress rounded-full bg-primary motion-reduce:animate-none" }) }));
|
|
110
|
+
}
|
|
111
|
+
function ariaSort(sortKey, sorts) {
|
|
112
|
+
const primary = sorts[0];
|
|
113
|
+
if (!sortKey || primary?.key !== sortKey)
|
|
114
|
+
return undefined;
|
|
115
|
+
return primary.dir === "asc" ? "ascending" : "descending";
|
|
116
|
+
}
|
|
117
|
+
function SortHeader({ sortKey, sorts, onToggle, children, }) {
|
|
118
|
+
const { messages } = useUI();
|
|
119
|
+
const index = sorts.findIndex((s) => s.key === sortKey);
|
|
120
|
+
const dir = index === -1 ? undefined : sorts[index]?.dir;
|
|
121
|
+
return (_jsxs("button", { type: "button", onClick: (event) => onToggle(sortKey, event.shiftKey), className: cn("inline-flex items-center gap-1 rounded-lg uppercase transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", dir ? "text-foreground" : "text-muted-foreground"), children: [children, dir === "asc" ? (_jsx(ArrowUpIcon, { className: "size-3.5", "aria-hidden": true })) : dir === "desc" ? (_jsx(ArrowDownIcon, { className: "size-3.5", "aria-hidden": true })) : (_jsx(ChevronsUpDownIcon, { className: "size-3.5 opacity-40", "aria-hidden": true })), index !== -1 && sorts.length > 1 && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, className: "font-mono text-2xs tabular-nums", children: index + 1 }), _jsxs("span", { className: "sr-only", children: [", ", messages.table.sortKey(index + 1)] })] }))] }));
|
|
122
|
+
}
|
|
123
|
+
function Toolbar({ controller, placeholder, children, }) {
|
|
124
|
+
const { messages } = useUI();
|
|
125
|
+
const t = messages.table;
|
|
126
|
+
const { search, setSearch, searchEnabled, totalCount } = controller;
|
|
127
|
+
if (!searchEnabled && !children && totalCount === null)
|
|
128
|
+
return null;
|
|
129
|
+
const searchLabel = placeholder ?? t.search;
|
|
130
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [searchEnabled && (_jsxs("div", { className: "relative min-w-56 flex-1 sm:max-w-xs", children: [_jsx(SearchIcon, { className: "pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { type: "search", value: search, onChange: (event) => setSearch(event.target.value), placeholder: searchLabel, "aria-label": searchLabel, className: "pr-8 pl-8 [&::-webkit-search-cancel-button]:appearance-none" }), search && (_jsx(Button, { variant: "ghost", size: "icon-xs", onClick: () => setSearch(""), "aria-label": t.clearSearch, className: "absolute top-1/2 right-1 -translate-y-1/2", children: _jsx(XIcon, { "aria-hidden": true }) }))] })), children, totalCount !== null && (_jsx("span", { className: "ml-auto text-sm text-muted-foreground tabular-nums", children: t.results(totalCount) }))] }));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Previous and next, never a page number to jump to: a keyset walk has no
|
|
134
|
+
* "page 7", so the control cannot be rendered honestly.
|
|
135
|
+
*/
|
|
136
|
+
function Pagination({ controller }) {
|
|
137
|
+
const { messages } = useUI();
|
|
138
|
+
const t = messages.table;
|
|
139
|
+
const selectId = React.useId();
|
|
140
|
+
const { pageIndex, hasNext, hasPrev, next, prev, pageSize, setPageSize } = controller;
|
|
141
|
+
const sizes = controller.pageSizes ?? PAGE_SIZES;
|
|
142
|
+
if (!hasNext && !hasPrev)
|
|
143
|
+
return null;
|
|
144
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center justify-end gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { htmlFor: selectId, className: "text-sm text-muted-foreground", children: t.rowsPerPage }), _jsx("select", { id: selectId, value: pageSize, onChange: (event) => setPageSize(Number(event.target.value)), className: "h-7 rounded-lg border border-input bg-transparent px-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50", children: sizes.map((size) => (_jsx("option", { value: size, children: size }, size))) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground tabular-nums", children: t.page(pageIndex + 1) }), _jsx(Button, { variant: "outline", size: "icon", onClick: prev, disabled: !hasPrev, "aria-label": t.previousPage, children: _jsx(ChevronLeftIcon, { "aria-hidden": true }) }), _jsx(Button, { variant: "outline", size: "icon", onClick: next, disabled: !hasNext, "aria-label": t.nextPage, children: _jsx(ChevronRightIcon, { "aria-hidden": true }) })] })] }));
|
|
145
|
+
}
|
|
146
|
+
function RowMenu({ label, children }) {
|
|
147
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": label, children: _jsx(MoreHorizontalIcon, { "aria-hidden": true }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "min-w-40", children: children })] }));
|
|
148
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { Density } from "../theme/tokens.js";
|
|
3
|
+
import type { EmptyStateSpec } from "./types.js";
|
|
4
|
+
export interface ListProps<TItem> {
|
|
5
|
+
/** Names the list for screen readers and in its error copy. */
|
|
6
|
+
label: string;
|
|
7
|
+
items: readonly TItem[];
|
|
8
|
+
getKey: (item: TItem) => string;
|
|
9
|
+
/** One item's content, usually a ListItem. The list draws the row or the card around it. */
|
|
10
|
+
renderItem: (item: TItem) => React.ReactNode;
|
|
11
|
+
/** Overrides the person's density for this list. */
|
|
12
|
+
density?: Density;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
/** How many placeholder items to draw while loading. */
|
|
15
|
+
loadingCount?: number;
|
|
16
|
+
error?: {
|
|
17
|
+
message: string;
|
|
18
|
+
} | null;
|
|
19
|
+
onRetry?: () => void;
|
|
20
|
+
/** Required: a list never renders nothing. */
|
|
21
|
+
empty: EmptyStateSpec;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A short, fixed list (integrations, links, a summary's top five) in the
|
|
26
|
+
* person's density: compact is one panel of hairline rows; cards boxes each
|
|
27
|
+
* item and flows them into responsive columns. Six links with a name and a
|
|
28
|
+
* sentence are a list, not six cards, until someone asks for cards.
|
|
29
|
+
*
|
|
30
|
+
* The layout is driven by CSS variables that `data-density` sets, so the
|
|
31
|
+
* first paint already has the right one and a subtree can override it. A
|
|
32
|
+
* list that can grow without bound is a DataTable, with server paging.
|
|
33
|
+
*/
|
|
34
|
+
export declare function List<TItem>({ label, items, getKey, renderItem, density, loading, loadingCount, error, onRetry, empty, className, }: ListProps<TItem>): React.JSX.Element;
|
|
35
|
+
export interface ListItemProps {
|
|
36
|
+
title: React.ReactNode;
|
|
37
|
+
/** The whole item links here; the title is the link, stretched over the item. */
|
|
38
|
+
href?: string;
|
|
39
|
+
description?: React.ReactNode;
|
|
40
|
+
/** Leading: an icon or a logo. */
|
|
41
|
+
icon?: React.ReactNode;
|
|
42
|
+
/** Trailing data: mono values, counts, times. */
|
|
43
|
+
meta?: React.ReactNode;
|
|
44
|
+
/** A StatusBadge. */
|
|
45
|
+
status?: React.ReactNode;
|
|
46
|
+
/** Controls, kept above the item's stretched link. */
|
|
47
|
+
actions?: React.ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One item's content: a row in compact density (icon, title and description,
|
|
52
|
+
* then meta, status and actions on the right) and a card in cards density
|
|
53
|
+
* (the same parts stacked).
|
|
54
|
+
*/
|
|
55
|
+
export declare function ListItem({ title, href, description, icon, meta, status, actions, className, }: ListItemProps): React.JSX.Element;
|