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

// Aubergine: triadic pair. Muted plum and ochre sit 120 degrees apart on the hue wheel. The third triadic hue, teal, is left out so the interface has two brand accents rather than three.
// Same values as docs/design/tokens.json; the theme test pins them to it.
export const interfaceVersion = 1;
export function theme() {
return {
displayName: "Aubergine",
tokens: {
light: {
"--canvas": "#f5f4f6",
"--surface": "#ffffff",
"--raised": "#ece8ed",
"--line": "#d9d2db",
"--border": "#78637e",
"--text": "#59455e",
"--muted": "#75607b",
"--action": "#88469b",
"--onAction": "#ffffff",
"--accent": "#77672c",
"--focus": "#88469b",
"--success": "#287658",
"--warning": "#8f5c19",
"--danger": "#ba2e26",
},
dim: {
"--canvas": "#312933",
"--surface": "#3e3441",
"--raised": "#4c3f50",
"--line": "#5f4f63",
"--border": "#b3a4b7",
"--text": "#e1d9e3",
"--muted": "#baacbe",
"--action": "#cba4d6",
"--onAction": "#10141a",
"--accent": "#d3c388",
"--focus": "#cba4d6",
"--success": "#85d5b7",
"--warning": "#e7b574",
"--danger": "#ea9d99",
},
dark: {
"--canvas": "#18141a",
"--surface": "#262028",
"--raised": "#362d39",
"--line": "#4c3f50",
"--border": "#b3a4b7",
"--text": "#c7b9cb",
"--muted": "#b3a4b7",
"--action": "#bf8ecc",
"--onAction": "#10141a",
"--accent": "#d3c388",
"--focus": "#bf8ecc",
"--success": "#85d5b7",
"--warning": "#e7b574",
"--danger": "#e37d78",
},
},
};
}