@calliopelabs/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +332 -2
  3. package/dist/appearance/AppearancePicker.d.ts +10 -0
  4. package/dist/appearance/AppearancePicker.js +75 -0
  5. package/dist/appearance/AppearanceProvider.d.ts +53 -0
  6. package/dist/appearance/AppearanceProvider.js +113 -0
  7. package/dist/appearance/AppearanceScript.d.ts +16 -0
  8. package/dist/appearance/AppearanceScript.js +14 -0
  9. package/dist/appearance/appearance.d.ts +90 -0
  10. package/dist/appearance/appearance.js +151 -0
  11. package/dist/appearance/boot.d.ts +30 -0
  12. package/dist/appearance/boot.js +91 -0
  13. package/dist/appearance/brands.d.ts +94 -0
  14. package/dist/appearance/brands.js +61 -0
  15. package/dist/appearance/product.d.ts +89 -0
  16. package/dist/appearance/product.js +96 -0
  17. package/dist/data/DataTable.d.ts +66 -0
  18. package/dist/data/DataTable.js +148 -0
  19. package/dist/data/List.d.ts +55 -0
  20. package/dist/data/List.js +41 -0
  21. package/dist/data/types.d.ts +92 -0
  22. package/dist/data/types.js +2 -0
  23. package/dist/data/use-row-selection.d.ts +17 -0
  24. package/dist/data/use-row-selection.js +51 -0
  25. package/dist/index.d.ts +50 -0
  26. package/dist/index.js +61 -0
  27. package/dist/lib/cn.d.ts +3 -0
  28. package/dist/lib/cn.js +6 -0
  29. package/dist/lib/local-store.d.ts +12 -0
  30. package/dist/lib/local-store.js +54 -0
  31. package/dist/nav/Nav.d.ts +21 -0
  32. package/dist/nav/Nav.js +97 -0
  33. package/dist/nav/model.d.ts +77 -0
  34. package/dist/nav/model.js +72 -0
  35. package/dist/overlays/ConfirmDialog.d.ts +37 -0
  36. package/dist/overlays/ConfirmDialog.js +70 -0
  37. package/dist/overlays/Dialog.d.ts +18 -0
  38. package/dist/overlays/Dialog.js +39 -0
  39. package/dist/overlays/Drawer.d.ts +24 -0
  40. package/dist/overlays/Drawer.js +42 -0
  41. package/dist/page/Breadcrumbs.d.ts +20 -0
  42. package/dist/page/Breadcrumbs.js +25 -0
  43. package/dist/page/PageFrame.d.ts +17 -0
  44. package/dist/page/PageFrame.js +14 -0
  45. package/dist/page/PageHeader.d.ts +28 -0
  46. package/dist/page/PageHeader.js +13 -0
  47. package/dist/page/Section.d.ts +18 -0
  48. package/dist/page/Section.js +12 -0
  49. package/dist/page/TabNav.d.ts +19 -0
  50. package/dist/page/TabNav.js +16 -0
  51. package/dist/primitives/Button.d.ts +16 -0
  52. package/dist/primitives/Button.js +36 -0
  53. package/dist/primitives/Checkbox.d.ts +8 -0
  54. package/dist/primitives/Checkbox.js +13 -0
  55. package/dist/primitives/DropdownMenu.d.ts +11 -0
  56. package/dist/primitives/DropdownMenu.js +25 -0
  57. package/dist/primitives/Input.d.ts +2 -0
  58. package/dist/primitives/Input.js +5 -0
  59. package/dist/primitives/Popover.d.ts +5 -0
  60. package/dist/primitives/Popover.js +13 -0
  61. package/dist/primitives/Skeleton.d.ts +3 -0
  62. package/dist/primitives/Skeleton.js +6 -0
  63. package/dist/primitives/Tabs.d.ts +12 -0
  64. package/dist/primitives/Tabs.js +22 -0
  65. package/dist/provider/UIProvider.d.ts +29 -0
  66. package/dist/provider/UIProvider.js +21 -0
  67. package/dist/provider/messages.d.ts +106 -0
  68. package/dist/provider/messages.js +104 -0
  69. package/dist/shell/AppShell.d.ts +43 -0
  70. package/dist/shell/AppShell.js +62 -0
  71. package/dist/shell/BrandMark.d.ts +14 -0
  72. package/dist/shell/BrandMark.js +14 -0
  73. package/dist/shell/RailPanel.d.ts +39 -0
  74. package/dist/shell/RailPanel.js +62 -0
  75. package/dist/shell/RailTree.d.ts +35 -0
  76. package/dist/shell/RailTree.js +32 -0
  77. package/dist/shell/Sidebar.d.ts +26 -0
  78. package/dist/shell/Sidebar.js +21 -0
  79. package/dist/shell/UserMenu.d.ts +22 -0
  80. package/dist/shell/UserMenu.js +31 -0
  81. package/dist/shell/placement.d.ts +10 -0
  82. package/dist/shell/placement.js +6 -0
  83. package/dist/shell/rail-state.d.ts +13 -0
  84. package/dist/shell/rail-state.js +28 -0
  85. package/dist/states/EmptyState.d.ts +20 -0
  86. package/dist/states/EmptyState.js +15 -0
  87. package/dist/states/ErrorState.d.ts +23 -0
  88. package/dist/states/ErrorState.js +16 -0
  89. package/dist/states/LoadingState.d.ts +14 -0
  90. package/dist/states/LoadingState.js +14 -0
  91. package/dist/status/StatusBadge.d.ts +19 -0
  92. package/dist/status/StatusBadge.js +13 -0
  93. package/dist/status/StatusDot.d.ts +14 -0
  94. package/dist/status/StatusDot.js +7 -0
  95. package/dist/status/tones.d.ts +17 -0
  96. package/dist/status/tones.js +29 -0
  97. package/dist/styles/preset.css +175 -0
  98. package/dist/styles/theme.css +339 -0
  99. package/dist/theme/contrast.d.ts +8 -0
  100. package/dist/theme/contrast.js +18 -0
  101. package/dist/theme/css.d.ts +23 -0
  102. package/dist/theme/css.js +116 -0
  103. package/dist/theme/tokens.d.ts +195 -0
  104. package/dist/theme/tokens.js +294 -0
  105. package/package.json +112 -4
@@ -0,0 +1,90 @@
1
+ import { type Density, type Ground, type Mode } from "../theme/tokens.js";
2
+ import { type Corners, type Product } from "./product.js";
3
+ /**
4
+ * Appearance: the axes a person sets for themselves, per product.
5
+ *
6
+ * ground the surface ramp; it implies light or dark (`data-mode`)
7
+ * accent the action colour and lead chart series: the product's accent,
8
+ * a family accent, or any `#rrggbb` that stands 3:1 off the ground
9
+ * density compact | cards
10
+ * corners 0 | 2 | 4 | 10 px
11
+ *
12
+ * A per-person display preference, not org state. It lives in localStorage
13
+ * because it must apply before first paint (AppearanceScript), which rules out
14
+ * waiting for a fetch; an app that also keeps it on its server hands it back
15
+ * through `AppearanceProvider`. Reads are defensive: Safari private mode and
16
+ * embedded webviews throw on storage access rather than returning null.
17
+ */
18
+ export type CustomAccent = `#${string}`;
19
+ export interface Appearance {
20
+ ground: Ground;
21
+ /** A product accent key, or a custom `#rrggbb`. */
22
+ accent: string;
23
+ density: Density;
24
+ corners: Corners;
25
+ }
26
+ /**
27
+ * Org policy: a house theme for people who have not chosen, optionally
28
+ * locked so the personal picker becomes read-only.
29
+ */
30
+ export interface OrgAppearancePolicy {
31
+ orgDefault?: Partial<Appearance> | null;
32
+ locked?: boolean;
33
+ }
34
+ export interface ResolvedAppearance {
35
+ value: Appearance;
36
+ mode: Mode;
37
+ /** True when the org locked the theme; the picker renders read-only. */
38
+ locked: boolean;
39
+ }
40
+ export declare function modeFor(ground: Ground): Mode;
41
+ /** A well-formed custom accent: lowercase `#rrggbb`. */
42
+ export declare function isCustomAccent(value: unknown): value is CustomAccent;
43
+ /** `#RGB`, `rrggbb` or `#RRGGBB` as a stored custom accent, or null. */
44
+ export declare function parseCustomAccent(raw: string): CustomAccent | null;
45
+ /** A custom accent's contrast against a ground's page colour, and whether it clears 3:1. */
46
+ export declare function accentContrast(accent: CustomAccent, ground: Ground): {
47
+ ratio: number;
48
+ ok: boolean;
49
+ };
50
+ /** Black or white, whichever reads better on a filled accent. */
51
+ export declare function inkFor(accent: CustomAccent): "#000000" | "#ffffff";
52
+ /**
53
+ * Keep only the axes that are present and valid for this product, without
54
+ * filling in the rest. A partial has to stay partial: an org that pins only
55
+ * the accent must not thereby pin the ground, and a stored `{accent}` must
56
+ * leave the other axes to the org default.
57
+ */
58
+ export declare function normalizePartial(raw: unknown, product: Product): Partial<Appearance>;
59
+ /**
60
+ * A complete appearance from anything. The ground falls back to the
61
+ * product's default for `systemMode`; a custom accent that cannot be seen on
62
+ * the ground it lands on falls back to the product's accent.
63
+ */
64
+ export declare function normalize(raw: unknown, product: Product, systemMode?: Mode): Appearance;
65
+ /**
66
+ * Precedence: a locked org theme beats everything; otherwise a personal
67
+ * choice beats the org default, which beats the product default. Until
68
+ * someone picks a ground, light or dark follows the operating system.
69
+ */
70
+ export declare function resolveAppearance(personal: Partial<Appearance> | null, policy: OrgAppearancePolicy | null, product: Product, systemMode?: Mode): ResolvedAppearance;
71
+ /** The stored preference as written, or null. Used as the external-store snapshot. */
72
+ export declare function readStoredAppearance(product: Product): string | null;
73
+ /** Parse a stored preference; null when nothing recognisable is stored. */
74
+ export declare function parseStoredAppearance(raw: string | null, product: Product): Partial<Appearance> | null;
75
+ /**
76
+ * The personal preference, or null when this person has never chosen.
77
+ * Returning null rather than a filled-in default is load-bearing: the org
78
+ * default only applies to people who have not chosen, and "never chose" must
79
+ * not look like "chose the defaults".
80
+ */
81
+ export declare function readAppearance(product: Product): Partial<Appearance> | null;
82
+ export declare function writeAppearance(product: Product, next: Partial<Appearance>): void;
83
+ /** Forget the personal preference, handing the decision back to the org or the product. */
84
+ export declare function clearAppearance(product: Product): void;
85
+ /** The preference as an app's server holds it; an empty object means never chosen. */
86
+ export declare function appearanceFromServer(raw: unknown, product: Product): Partial<Appearance> | null;
87
+ /** The tokens a custom accent sets inline, because it has no CSS block. */
88
+ export declare const CUSTOM_ACCENT_PROPERTIES: readonly ["--brand-primary", "--brand-on-primary"];
89
+ /** Stamp the axes onto <html> (or `root`). Safe to call before React hydrates. */
90
+ export declare function applyAppearance(value: Appearance, product: Product, root?: HTMLElement): void;
@@ -0,0 +1,151 @@
1
+ import { readLocal, writeLocal } from "../lib/local-store.js";
2
+ import { MIN_NON_TEXT_CONTRAST, contrastRatio } from "../theme/contrast.js";
3
+ import { GROUNDS, SURFACES } from "../theme/tokens.js";
4
+ import { CORNERS } from "./product.js";
5
+ const HEX = /^#[0-9a-f]{6}$/;
6
+ export function modeFor(ground) {
7
+ return GROUNDS[ground].mode;
8
+ }
9
+ /** A well-formed custom accent: lowercase `#rrggbb`. */
10
+ export function isCustomAccent(value) {
11
+ return typeof value === "string" && HEX.test(value);
12
+ }
13
+ /** `#RGB`, `rrggbb` or `#RRGGBB` as a stored custom accent, or null. */
14
+ export function parseCustomAccent(raw) {
15
+ let value = raw.trim().toLowerCase();
16
+ if (!value.startsWith("#"))
17
+ value = `#${value}`;
18
+ if (/^#[0-9a-f]{3}$/.test(value)) {
19
+ value = `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`;
20
+ }
21
+ return isCustomAccent(value) ? value : null;
22
+ }
23
+ /** A custom accent's contrast against a ground's page colour, and whether it clears 3:1. */
24
+ export function accentContrast(accent, ground) {
25
+ const ratio = contrastRatio(accent, SURFACES[ground].background);
26
+ return { ratio, ok: ratio >= MIN_NON_TEXT_CONTRAST };
27
+ }
28
+ /** Black or white, whichever reads better on a filled accent. */
29
+ export function inkFor(accent) {
30
+ return contrastRatio(accent, "#000000") >= contrastRatio(accent, "#ffffff")
31
+ ? "#000000"
32
+ : "#ffffff";
33
+ }
34
+ function isGround(product, value) {
35
+ return typeof value === "string" && product.grounds.includes(value);
36
+ }
37
+ function isProductAccent(product, value) {
38
+ return typeof value === "string" && product.accents.some((a) => a.key === value);
39
+ }
40
+ function isCorners(value) {
41
+ return CORNERS.includes(value);
42
+ }
43
+ /**
44
+ * Keep only the axes that are present and valid for this product, without
45
+ * filling in the rest. A partial has to stay partial: an org that pins only
46
+ * the accent must not thereby pin the ground, and a stored `{accent}` must
47
+ * leave the other axes to the org default.
48
+ */
49
+ export function normalizePartial(raw, product) {
50
+ const v = (typeof raw === "object" && raw !== null ? raw : {});
51
+ const out = {};
52
+ if (isGround(product, v.ground))
53
+ out.ground = v.ground;
54
+ if (isProductAccent(product, v.accent) || isCustomAccent(v.accent))
55
+ out.accent = v.accent;
56
+ if (v.density === "compact" || v.density === "cards")
57
+ out.density = v.density;
58
+ if (isCorners(v.corners))
59
+ out.corners = v.corners;
60
+ return out;
61
+ }
62
+ /**
63
+ * A complete appearance from anything. The ground falls back to the
64
+ * product's default for `systemMode`; a custom accent that cannot be seen on
65
+ * the ground it lands on falls back to the product's accent.
66
+ */
67
+ export function normalize(raw, product, systemMode = "dark") {
68
+ const v = normalizePartial(raw, product);
69
+ const ground = v.ground ?? product.defaults.ground[systemMode];
70
+ const accent = v.accent !== undefined && (!isCustomAccent(v.accent) || accentContrast(v.accent, ground).ok)
71
+ ? v.accent
72
+ : product.defaults.accent;
73
+ return {
74
+ ground,
75
+ accent,
76
+ density: v.density ?? product.defaults.density,
77
+ corners: v.corners ?? product.defaults.corners,
78
+ };
79
+ }
80
+ /**
81
+ * Precedence: a locked org theme beats everything; otherwise a personal
82
+ * choice beats the org default, which beats the product default. Until
83
+ * someone picks a ground, light or dark follows the operating system.
84
+ */
85
+ export function resolveAppearance(personal, policy, product, systemMode = "dark") {
86
+ const orgDefault = normalizePartial(policy?.orgDefault ?? null, product);
87
+ const locked = Boolean(policy?.locked);
88
+ const merged = locked
89
+ ? orgDefault
90
+ : { ...orgDefault, ...normalizePartial(personal ?? {}, product) };
91
+ const value = normalize(merged, product, systemMode);
92
+ return { value, mode: modeFor(value.ground), locked };
93
+ }
94
+ /** The stored preference as written, or null. Used as the external-store snapshot. */
95
+ export function readStoredAppearance(product) {
96
+ return readLocal(product.storageKey);
97
+ }
98
+ /** Parse a stored preference; null when nothing recognisable is stored. */
99
+ export function parseStoredAppearance(raw, product) {
100
+ if (!raw)
101
+ return null;
102
+ try {
103
+ const parsed = normalizePartial(JSON.parse(raw), product);
104
+ return Object.keys(parsed).length > 0 ? parsed : null;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * The personal preference, or null when this person has never chosen.
112
+ * Returning null rather than a filled-in default is load-bearing: the org
113
+ * default only applies to people who have not chosen, and "never chose" must
114
+ * not look like "chose the defaults".
115
+ */
116
+ export function readAppearance(product) {
117
+ return parseStoredAppearance(readStoredAppearance(product), product);
118
+ }
119
+ export function writeAppearance(product, next) {
120
+ writeLocal(product.storageKey, JSON.stringify(normalizePartial(next, product)));
121
+ }
122
+ /** Forget the personal preference, handing the decision back to the org or the product. */
123
+ export function clearAppearance(product) {
124
+ writeLocal(product.storageKey, null);
125
+ }
126
+ /** The preference as an app's server holds it; an empty object means never chosen. */
127
+ export function appearanceFromServer(raw, product) {
128
+ const parsed = normalizePartial(raw, product);
129
+ return Object.keys(parsed).length > 0 ? parsed : null;
130
+ }
131
+ /** The tokens a custom accent sets inline, because it has no CSS block. */
132
+ export const CUSTOM_ACCENT_PROPERTIES = ["--brand-primary", "--brand-on-primary"];
133
+ /** Stamp the axes onto <html> (or `root`). Safe to call before React hydrates. */
134
+ export function applyAppearance(value, product, root) {
135
+ const el = root ?? document.documentElement;
136
+ el.dataset.product = product.id;
137
+ el.dataset.ground = value.ground;
138
+ el.dataset.mode = modeFor(value.ground);
139
+ if (isCustomAccent(value.accent)) {
140
+ el.dataset.accent = "custom";
141
+ el.style.setProperty("--brand-primary", value.accent);
142
+ el.style.setProperty("--brand-on-primary", inkFor(value.accent));
143
+ }
144
+ else {
145
+ el.dataset.accent = value.accent;
146
+ for (const property of CUSTOM_ACCENT_PROPERTIES)
147
+ el.style.removeProperty(property);
148
+ }
149
+ el.dataset.density = value.density;
150
+ el.style.setProperty("--radius", `${value.corners}px`);
151
+ }
@@ -0,0 +1,30 @@
1
+ import { type Mode } from "../theme/tokens.js";
2
+ import { type Product } from "./product.js";
3
+ /** What the boot script needs to know about a product: plain data, serialised into the page. */
4
+ export interface BootConfig {
5
+ product: string;
6
+ key: string;
7
+ grounds: Record<string, {
8
+ mode: Mode;
9
+ background: string;
10
+ }>;
11
+ accents: string[];
12
+ corners: number[];
13
+ minContrast: number;
14
+ defaults: {
15
+ dark: string;
16
+ light: string;
17
+ accent: string;
18
+ density: string;
19
+ corners: number;
20
+ };
21
+ }
22
+ export declare function bootConfig(product: Product): BootConfig;
23
+ /**
24
+ * The inline script that stamps a product's appearance on <html> before
25
+ * first paint. Render it in <head> (AppearanceScript does), or write it into
26
+ * an HTML template yourself.
27
+ */
28
+ export declare function appearanceScript(product: Product): string;
29
+ /** The product's brand tokens as a CSS string: its sidebar surface and its own accent. */
30
+ export declare function productStyles(product: Product): string;
@@ -0,0 +1,91 @@
1
+ import { renderProductCss } from "../theme/css.js";
2
+ import { MIN_NON_TEXT_CONTRAST } from "../theme/contrast.js";
3
+ import { GROUNDS, SURFACES } from "../theme/tokens.js";
4
+ import { CORNERS } from "./product.js";
5
+ export function bootConfig(product) {
6
+ return {
7
+ product: product.id,
8
+ key: product.storageKey,
9
+ grounds: Object.fromEntries(product.grounds.map((g) => [g, { mode: GROUNDS[g].mode, background: SURFACES[g].background }])),
10
+ accents: product.accents.map((a) => a.key),
11
+ corners: [...CORNERS],
12
+ minContrast: MIN_NON_TEXT_CONTRAST,
13
+ defaults: {
14
+ dark: product.defaults.ground.dark,
15
+ light: product.defaults.ground.light,
16
+ accent: product.defaults.accent,
17
+ density: product.defaults.density,
18
+ corners: product.defaults.corners,
19
+ },
20
+ };
21
+ }
22
+ /**
23
+ * The boot script, as source text: plain ES5 in a string, so no bundler,
24
+ * transpiler or coverage tool can rewrite it into something that needs a
25
+ * helper from outside. It is `normalize` + `resolveAppearance` (with no org
26
+ * policy, which cannot be known before paint) + `applyAppearance`, written a
27
+ * second time; boot.test.ts runs it against those over a matrix of stored
28
+ * preferences and system modes, so the two cannot drift apart.
29
+ */
30
+ const BOOT_SOURCE = `function (c) {
31
+ var d = document.documentElement, p = {};
32
+ try {
33
+ var raw = window.localStorage.getItem(c.key);
34
+ var parsed = raw ? JSON.parse(raw) : null;
35
+ if (parsed && typeof parsed === "object") p = parsed;
36
+ } catch (e) {}
37
+ var light = false;
38
+ try {
39
+ light = window.matchMedia("(prefers-color-scheme: light)").matches;
40
+ } catch (e) {}
41
+ var has = function (o, k) {
42
+ return typeof k === "string" && Object.prototype.hasOwnProperty.call(o, k);
43
+ };
44
+ var lum = function (h) {
45
+ var n = parseInt(h.slice(1), 16), rgb = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
46
+ var w = [0.2126, 0.7152, 0.0722], s = 0;
47
+ for (var i = 0; i < 3; i++) {
48
+ var v = rgb[i] / 255;
49
+ s += w[i] * (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
50
+ }
51
+ return s;
52
+ };
53
+ var ratio = function (x, y) {
54
+ var a = lum(x), b = lum(y);
55
+ return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
56
+ };
57
+ var ground = has(c.grounds, p.ground) ? p.ground : light ? c.defaults.light : c.defaults.dark;
58
+ var accent = p.accent;
59
+ var custom = typeof accent === "string" && /^#[0-9a-f]{6}$/.test(accent) &&
60
+ ratio(accent, c.grounds[ground].background) >= c.minContrast;
61
+ d.dataset.product = c.product;
62
+ d.dataset.ground = ground;
63
+ d.dataset.mode = c.grounds[ground].mode;
64
+ if (custom) {
65
+ d.dataset.accent = "custom";
66
+ d.style.setProperty("--brand-primary", accent);
67
+ d.style.setProperty("--brand-on-primary",
68
+ ratio(accent, "#000000") >= ratio(accent, "#ffffff") ? "#000000" : "#ffffff");
69
+ } else {
70
+ d.dataset.accent = c.accents.indexOf(accent) > -1 ? accent : c.defaults.accent;
71
+ }
72
+ d.dataset.density = p.density === "compact" || p.density === "cards" ? p.density : c.defaults.density;
73
+ d.style.setProperty("--radius",
74
+ (c.corners.indexOf(p.corners) > -1 ? p.corners : c.defaults.corners) + "px");
75
+ }`;
76
+ /** JSON that is safe inside an inline <script>. */
77
+ function scriptJson(value) {
78
+ return JSON.stringify(value).replace(/</g, "\\u003c");
79
+ }
80
+ /**
81
+ * The inline script that stamps a product's appearance on <html> before
82
+ * first paint. Render it in <head> (AppearanceScript does), or write it into
83
+ * an HTML template yourself.
84
+ */
85
+ export function appearanceScript(product) {
86
+ return `try{(${BOOT_SOURCE})(${scriptJson(bootConfig(product))})}catch(e){}`;
87
+ }
88
+ /** The product's brand tokens as a CSS string: its sidebar surface and its own accent. */
89
+ export function productStyles(product) {
90
+ return renderProductCss({ sidebar: product.sidebar, brandAccent: product.brandAccent });
91
+ }
@@ -0,0 +1,94 @@
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 declare const astroliftBrand: {
13
+ readonly id: "astrolift";
14
+ readonly name: "Astrolift AI";
15
+ readonly accent: "green";
16
+ readonly sidebar: "#04231a";
17
+ readonly grounds: readonly ["black", "charcoal", "graphite", "emerald", "paper", "mist"];
18
+ readonly defaultGround: {
19
+ readonly dark: "graphite";
20
+ readonly light: "mist";
21
+ };
22
+ readonly themes: readonly [{
23
+ readonly id: "orbit";
24
+ readonly name: "Orbit";
25
+ readonly hint: "Black · green";
26
+ readonly ground: "black";
27
+ readonly accent: "green";
28
+ }, {
29
+ readonly id: "forge";
30
+ readonly name: "Forge";
31
+ readonly hint: "Black · copper";
32
+ readonly ground: "black";
33
+ readonly accent: "copper";
34
+ }, {
35
+ readonly id: "console";
36
+ readonly name: "Console";
37
+ readonly hint: "Charcoal · ice";
38
+ readonly ground: "charcoal";
39
+ readonly accent: "ice";
40
+ }, {
41
+ readonly id: "dusk";
42
+ readonly name: "Dusk";
43
+ readonly hint: "Charcoal · periwinkle";
44
+ readonly ground: "charcoal";
45
+ readonly accent: "periwinkle";
46
+ }, {
47
+ readonly id: "graphite";
48
+ readonly name: "Graphite";
49
+ readonly hint: "Graphite · green";
50
+ readonly ground: "graphite";
51
+ readonly accent: "green";
52
+ }, {
53
+ readonly id: "field";
54
+ readonly name: "Field";
55
+ readonly hint: "Emerald · green";
56
+ readonly ground: "emerald";
57
+ readonly accent: "green";
58
+ }, {
59
+ readonly id: "daylight";
60
+ readonly name: "Daylight";
61
+ readonly hint: "Mist · green";
62
+ readonly ground: "mist";
63
+ readonly accent: "green";
64
+ }, {
65
+ readonly id: "workshop";
66
+ readonly name: "Workshop";
67
+ readonly hint: "Paper · copper";
68
+ readonly ground: "paper";
69
+ readonly accent: "copper";
70
+ }];
71
+ };
72
+ /**
73
+ * Zentinelle AI: teal on the navy sidebar, on a true black ground
74
+ * (ui-overhaul-ia §5). The family accents stay selectable beside teal. The
75
+ * emerald ground is Astrolift's planet green and is not offered.
76
+ */
77
+ export declare const zentinelleBrand: {
78
+ readonly id: "zentinelle";
79
+ readonly name: "Zentinelle AI";
80
+ readonly accent: {
81
+ readonly key: "teal";
82
+ readonly label: "Teal";
83
+ readonly dark: "#37efed";
84
+ readonly onDark: "#0b0b19";
85
+ readonly light: "#0d7470";
86
+ readonly onLight: "#f3fbf7";
87
+ };
88
+ readonly sidebar: "#0b0b19";
89
+ readonly grounds: readonly ["black", "charcoal", "graphite", "paper", "mist"];
90
+ readonly defaultGround: {
91
+ readonly dark: "black";
92
+ readonly light: "mist";
93
+ };
94
+ };
@@ -0,0 +1,61 @@
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
+ };
@@ -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;