Files
jason.woltje 7fdb488270 Themes as installed plugins, chosen per organization (S4, plan C5)
Add a reviewed `theme` plugin kind (interfaceVersion 1, export `theme`, switch
"list"). A module returns data only: a displayName and a flat map of colour
custom properties for light, dim and dark. checkTheme validates names and
#rrggbb colours strictly and checks each text/background pair for contrast; a
bad module is refused and logged, never served.

The ten shipped palettes become the first ten modules under
packages/plugins/theme/<name>/. Modules are found by directory scan
(loader.discoverModules, loader.loadInstalled), not by a list in the Console.
loadModule and createLoader are unchanged.

The organization file takes `appearance: { theme, mode }`, validated in
packages/business/src/appearance.mjs; a missing appearance is valid.
GET /api/themes (themes-http.mjs) serves the installed, valid themes.

Browser: the Settings picker lists /api/themes. Precedence is a per-organization
browser override, then the organization's saved theme, then the built-in default
(Harbor, dark). Clearing the override restores the organization's choice. An
uninstalled or invalid saved theme renders the default with a visible warning
and keeps the saved value. A legacy global browser preference is not copied to
every organization. The organization is named in text, so colour is never the
only cue. A plain load writes no appearance record. The default look is
unchanged: computed styles match the base commit element for element.
2026-11-07 12:18:43 -06:00

60 lines
1.7 KiB
JavaScript

// Terracotta: complementary. Fired-clay orange meets a desaturated steel blue. Warm neutral surfaces connect the palette without needing a beige wash everywhere.
// Same values as docs/design/tokens.json; the theme test pins them to it.
export const interfaceVersion = 1;
export function theme() {
return {
displayName: "Terracotta",
tokens: {
light: {
"--canvas": "#f7f4f3",
"--surface": "#ffffff",
"--raised": "#eee9e7",
"--line": "#ded3cf",
"--border": "#7e6b63",
"--text": "#584841",
"--muted": "#78665e",
"--action": "#a74f2a",
"--onAction": "#ffffff",
"--accent": "#356f88",
"--focus": "#a74f2a",
"--success": "#287658",
"--warning": "#8f5c19",
"--danger": "#ba2e26",
},
dim: {
"--canvas": "#362b26",
"--surface": "#453630",
"--raised": "#54423b",
"--line": "#695349",
"--border": "#b7a9a4",
"--text": "#e5dfdc",
"--muted": "#beb2ac",
"--action": "#e2a68d",
"--onAction": "#10141a",
"--accent": "#89bcd1",
"--focus": "#e2a68d",
"--success": "#85d5b7",
"--warning": "#e7b574",
"--danger": "#eba29e",
},
dark: {
"--canvas": "#1b1513",
"--surface": "#2a211d",
"--raised": "#3c2f2a",
"--line": "#54423b",
"--border": "#b7a9a4",
"--text": "#cbbeb9",
"--muted": "#b7a9a4",
"--action": "#de9a7c",
"--onAction": "#10141a",
"--accent": "#89bcd1",
"--focus": "#de9a7c",
"--success": "#85d5b7",
"--warning": "#e7b574",
"--danger": "#e37d78",
},
},
};
}