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.
60 lines
1.7 KiB
JavaScript
60 lines
1.7 KiB
JavaScript
// Mineral: complementary. Deep teal and dusty rose oppose each other while keeping saturation restrained. Blue-green tinted layers carry the identity in dark mode.
|
|
// Same values as docs/design/tokens.json; the theme test pins them to it.
|
|
export const interfaceVersion = 1;
|
|
|
|
export function theme() {
|
|
return {
|
|
displayName: "Mineral",
|
|
tokens: {
|
|
light: {
|
|
"--canvas": "#f3f6f6",
|
|
"--surface": "#ffffff",
|
|
"--raised": "#e8eded",
|
|
"--line": "#d0dcda",
|
|
"--border": "#637e7a",
|
|
"--text": "#3d524f",
|
|
"--muted": "#586f6c",
|
|
"--action": "#2a7469",
|
|
"--onAction": "#ffffff",
|
|
"--accent": "#9f414f",
|
|
"--focus": "#2a7469",
|
|
"--success": "#287658",
|
|
"--warning": "#935f1a",
|
|
"--danger": "#ba2e26",
|
|
},
|
|
dim: {
|
|
"--canvas": "#273432",
|
|
"--surface": "#324340",
|
|
"--raised": "#3d514e",
|
|
"--line": "#4d6662",
|
|
"--border": "#a4b7b4",
|
|
"--text": "#e5ebea",
|
|
"--muted": "#b2c2c0",
|
|
"--action": "#87d4c8",
|
|
"--onAction": "#10141a",
|
|
"--accent": "#e0b3ba",
|
|
"--focus": "#87d4c8",
|
|
"--success": "#85d5b7",
|
|
"--warning": "#e7b574",
|
|
"--danger": "#eeaeaa",
|
|
},
|
|
dark: {
|
|
"--canvas": "#141a19",
|
|
"--surface": "#1f2927",
|
|
"--raised": "#2c3a38",
|
|
"--line": "#3d514e",
|
|
"--border": "#a4b7b4",
|
|
"--text": "#b9cbc8",
|
|
"--muted": "#a4b7b4",
|
|
"--action": "#87d4c8",
|
|
"--onAction": "#10141a",
|
|
"--accent": "#d18f99",
|
|
"--focus": "#87d4c8",
|
|
"--success": "#85d5b7",
|
|
"--warning": "#e7b574",
|
|
"--danger": "#e58580",
|
|
},
|
|
},
|
|
};
|
|
}
|