Centralize color management (#1135)

This commit is contained in:
David Straub
2026-05-07 15:25:59 +02:00
committed by GitHub
parent dda113ae1c
commit 37b927942c
8 changed files with 222 additions and 130 deletions
+3 -1
View File
@@ -42,8 +42,10 @@ src/
gramps-js.js # Entry point (registers the custom element) gramps-js.js # Entry point (registers the custom element)
api.js # All API calls, auth helpers, localStorage utilities api.js # All API calls, auth helpers, localStorage utilities
appState.js # App-wide state initialisation appState.js # App-wide state initialisation
color.js # Gramps internal hex format converters (tag colours etc.)
config.js # Runtime config (window.grampsjsConfig); see below config.js # Runtime config (window.grampsjsConfig); see below
strings.js # i18n string keys and language list strings.js # i18n string keys and language list
theme.js # Colour theme: applyScheme(), applyColors(), DEFAULT_PRIMARY/SECONDARY
util.js # Shared helpers util.js # Shared helpers
components/ # Reusable components components/ # Reusable components
views/ # Page-level components views/ # Page-level components
@@ -80,7 +82,7 @@ html`<grampsjs-icon path="${mdiInformation}"></grampsjs-icon>`
Optional attributes: `color`, `height`, `width`, `rotate`. Optional attributes: `color`, `height`, `width`, `rotate`.
For action icon color (add/edit/delete buttons), use `color="var(--mdc-theme-secondary)"` — this is `#0277bd` and is consistent across light and dark mode. For action icon color (add/edit/delete buttons), use `color="var(--mdc-theme-secondary)"` — this is derived from the active secondary seed colour and adapts to light/dark mode.
### Mixins ### Mixins
+77 -121
View File
@@ -11,8 +11,7 @@ body {
/* Web Awesome color token overrides. /* Web Awesome color token overrides.
default.css (loaded in index.html) defines --wa-* tokens inside @layer wa-theme, default.css (loaded in index.html) defines --wa-* tokens inside @layer wa-theme,
so these unlayered declarations win without !important. The --md-sys-color-* so these unlayered declarations win without !important. The --md-sys-color-*
values on the right are defined in the light/dark blocks below, so the values on the right are set dynamically by applyColors() in theme.js. */
color picker automatically adapts to the active theme. */
--wa-color-surface-raised: var(--md-sys-color-surface-container-high); --wa-color-surface-raised: var(--md-sys-color-surface-container-high);
--wa-color-surface-default: var(--md-sys-color-surface); --wa-color-surface-default: var(--md-sys-color-surface);
--wa-color-surface-border: var(--md-sys-color-outline-variant); --wa-color-surface-border: var(--md-sys-color-outline-variant);
@@ -29,7 +28,7 @@ body {
--color-other: #ce93d8; --color-other: #ce93d8;
--mdc-theme-primary: var(--md-sys-color-primary); --mdc-theme-primary: var(--md-sys-color-primary);
--mdc-theme-on-primary: var(--md-sys-color-on-primary); --mdc-theme-on-primary: var(--md-sys-color-on-primary);
--mdc-theme-secondary: #0277bd; --mdc-theme-secondary: var(--md-sys-color-secondary);
--mdc-theme-on-secondary: rgba(255, 255, 255, 0.95); --mdc-theme-on-secondary: rgba(255, 255, 255, 0.95);
--mdc-theme-background: var(--md-sys-color-background); --mdc-theme-background: var(--md-sys-color-background);
--mdc-theme-surface: var(--md-sys-color-surface); --mdc-theme-surface: var(--md-sys-color-surface);
@@ -49,11 +48,27 @@ body {
--grampsjs-lightbox-font-color: #fff; --grampsjs-lightbox-font-color: #fff;
--grampsjs-lightbox-nav-color: #aaa; --grampsjs-lightbox-nav-color: #aaa;
--grampsjs-lightbox-nav-hover-color: #fff; --grampsjs-lightbox-nav-hover-color: #fff;
--grampsjs-alert-background-color: rgba(109, 76, 65, 0.15); --grampsjs-alert-background-color: color-mix(
--grampsjs-alert-border-color: rgba(109, 76, 65, 0.7); in srgb,
--grampsjs-alert-error-background-color: rgba(191, 54, 12, 0.2); var(--md-sys-color-primary) 15%,
--grampsjs-alert-error-border-color: rgba(191, 54, 12, 0.7); transparent
--grampsjs-alert-error-font-color: #bf360c; );
--grampsjs-alert-border-color: color-mix(
in srgb,
var(--md-sys-color-primary) 70%,
transparent
);
--grampsjs-alert-error-background-color: color-mix(
in srgb,
var(--md-sys-color-error) 20%,
transparent
);
--grampsjs-alert-error-border-color: color-mix(
in srgb,
var(--md-sys-color-error) 70%,
transparent
);
--grampsjs-alert-error-font-color: var(--md-sys-color-error);
--grampsjs-map-marker-color: #ea4335; --grampsjs-map-marker-color: #ea4335;
--grampsjs-map-filter-badge-color: #ba1b1b; --grampsjs-map-filter-badge-color: #ba1b1b;
--grampsjs-logout-font-color: #c62828; --grampsjs-logout-font-color: #c62828;
@@ -64,16 +79,59 @@ body {
--grampsjs-task-open: #007bff; --grampsjs-task-open: #007bff;
--grampsjs-task-progress: #ff9800; --grampsjs-task-progress: #ff9800;
--grampsjs-task-done: rgba(65, 173, 73, 0.4); --grampsjs-task-done: rgba(65, 173, 73, 0.4);
--grampsjs-editable-title-hover-background-color: rgba(2, 119, 189, 0.2); --grampsjs-editable-title-hover-background-color: color-mix(
--grampsjs-delete-all-filter-chip-color: rgba(109, 76, 65, 0.32); in srgb,
--grampsjs-connection-chart-border-color: rgba(137, 107, 94, 0.4); var(--mdc-theme-secondary) 20%,
transparent
);
--grampsjs-delete-all-filter-chip-color: color-mix(
in srgb,
var(--md-sys-color-primary) 32%,
transparent
);
--grampsjs-connection-chart-border-color: color-mix(
in srgb,
var(--md-sys-color-primary) 40%,
transparent
);
--grampsjs-connection-chart-person-box: #e6e6e6; --grampsjs-connection-chart-person-box: #e6e6e6;
--grampsjs-editable-list-hover-background-color: rgba(2, 119, 189, 0.1); --grampsjs-editable-list-hover-background-color: color-mix(
--grampsjs-editable-list-active-background-color: rgba(2, 119, 189, 0.2); in srgb,
--grampsjs-editable-list-selected-active-background-color: rgba(2, 119, 189, 0.3); var(--mdc-theme-secondary) 10%,
--grampsjs-editable-list-selected-hover-background-color: rgba(2, 119, 189, 0.4); transparent
--grampsjs-editable-list-selected-background-color: rgba(2, 119, 189, 0.5); );
--grampsjs-mobile-table-sort-menu-color: rgba(109, 76, 65, 0.3); --grampsjs-editable-list-active-background-color: color-mix(
in srgb,
var(--mdc-theme-secondary) 20%,
transparent
);
--grampsjs-editable-list-selected-active-background-color: color-mix(
in srgb,
var(--mdc-theme-secondary) 30%,
transparent
);
--grampsjs-editable-list-selected-hover-background-color: color-mix(
in srgb,
var(--mdc-theme-secondary) 40%,
transparent
);
--grampsjs-editable-list-selected-background-color: color-mix(
in srgb,
var(--mdc-theme-secondary) 50%,
transparent
);
--grampsjs-mobile-table-sort-menu-color: color-mix(
in srgb,
var(--md-sys-color-primary) 30%,
transparent
);
--grampsjs-color-link-font: var(--mdc-theme-secondary);
--grampsjs-color-link-hover: color-mix(
in srgb,
var(--mdc-theme-secondary) 80%,
black
);
--grampsjs-color-page-loading-progress: var(--mdc-theme-secondary);
} }
:root[data-theme='light'] { :root[data-theme='light'] {
@@ -116,9 +174,6 @@ body {
--grampsjs-color-skeleton-background: #eee; --grampsjs-color-skeleton-background: #eee;
--grampsjs-color-skeleton-base: #ececec; --grampsjs-color-skeleton-base: #ececec;
--grampsjs-color-skeleton-shine: #f5f5f5; --grampsjs-color-skeleton-shine: #f5f5f5;
--grampsjs-color-link-font: #0d47a1;
--grampsjs-color-link-hover: #1976d2;
--grampsjs-color-page-loading-progress: #4fc3f7;
--grampsjs-color-revision-deleted-background-red: #ffebe9; --grampsjs-color-revision-deleted-background-red: #ffebe9;
--grampsjs-color-revision-added-background-green: #dafbe1; --grampsjs-color-revision-added-background-green: #dafbe1;
--grampsjs-color-revision-moved-background-blue: #ddf4ff; --grampsjs-color-revision-moved-background-blue: #ddf4ff;
@@ -131,55 +186,7 @@ body {
--grampsjs-color-ytree-expand-font: #555; --grampsjs-color-ytree-expand-font: #555;
--grampsjs-color-ytree-default-person-icon-background: #bbb; --grampsjs-color-ytree-default-person-icon-background: #bbb;
--grampsjs-note-color: #333; --grampsjs-note-color: #333;
--md-sys-color-primary: rgb(109, 76, 65); /* --md-sys-color-* tokens are set dynamically by applyColors() in theme.js */
--md-sys-color-surface-tint: rgb(109, 76, 65);
--md-sys-color-on-primary: rgb(255, 255, 255);
--md-sys-color-primary-container: rgb(255, 219, 207);
--md-sys-color-on-primary-container: rgb(57, 12, 0);
--md-sys-color-secondary: rgb(49, 98, 141);
--md-sys-color-on-secondary: rgb(255, 255, 255);
--md-sys-color-secondary-container: rgb(207, 229, 255);
--md-sys-color-on-secondary-container: rgb(0, 29, 52);
--md-sys-color-tertiary: rgb(106, 94, 47);
--md-sys-color-on-tertiary: rgb(255, 255, 255);
--md-sys-color-tertiary-container: rgb(243, 226, 167);
--md-sys-color-on-tertiary-container: rgb(34, 27, 0);
--md-sys-color-error: rgb(186, 26, 26);
--md-sys-color-on-error: rgb(255, 255, 255);
--md-sys-color-error-container: rgb(255, 218, 214);
--md-sys-color-on-error-container: rgb(65, 0, 2);
--md-sys-color-background: #fff;;
--md-sys-color-on-background: var(--grampsjs-body-font-color);
--md-sys-color-surface: #fff;
--md-sys-color-on-surface: var(--grampsjs-body-font-color);
--md-sys-color-surface-variant: rgb(232, 224, 222);
--md-sys-color-on-surface-variant: rgb(83, 67, 63);
--md-sys-color-outline: rgb(133, 115, 110);
--md-sys-color-outline-variant: rgb(200, 200, 200);
--md-sys-color-shadow: rgb(0, 0, 0);
--md-sys-color-scrim: rgb(0, 0, 0);
--md-sys-color-inverse-surface: rgb(57, 46, 43);
--md-sys-color-inverse-on-surface: rgb(255, 237, 232);
--md-sys-color-inverse-primary: rgb(255, 181, 156);
--md-sys-color-primary-fixed: rgb(255, 219, 207);
--md-sys-color-on-primary-fixed: rgb(57, 12, 0);
--md-sys-color-primary-fixed-dim: rgb(255, 181, 156);
--md-sys-color-on-primary-fixed-variant: rgb(114, 53, 32);
--md-sys-color-secondary-fixed: rgb(207, 229, 255);
--md-sys-color-on-secondary-fixed: rgb(0, 29, 52);
--md-sys-color-secondary-fixed-dim: rgb(157, 203, 251);
--md-sys-color-on-secondary-fixed-variant: rgb(18, 74, 115);
--md-sys-color-tertiary-fixed: rgb(243, 226, 167);
--md-sys-color-on-tertiary-fixed: rgb(34, 27, 0);
--md-sys-color-tertiary-fixed-dim: rgb(214, 198, 141);
--md-sys-color-on-tertiary-fixed-variant: rgb(81, 70, 26);
--md-sys-color-surface-dim: rgb(232, 214, 209);
--md-sys-color-surface-bright: rgb(255, 248, 246);
--md-sys-color-surface-container-lowest: rgb(245, 245, 245);
--md-sys-color-surface-container-low: rgb(240, 240, 240);
--md-sys-color-surface-container: #fff;
--md-sys-color-surface-container-high: #fff;
--md-sys-color-surface-container-highest: whitesmoke;
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
@@ -222,9 +229,6 @@ body {
--grampsjs-color-skeleton-background: #222; --grampsjs-color-skeleton-background: #222;
--grampsjs-color-skeleton-base: #232323; --grampsjs-color-skeleton-base: #232323;
--grampsjs-color-skeleton-shine: #1a1a1a; --grampsjs-color-skeleton-shine: #1a1a1a;
--grampsjs-color-link-font: #5193f6;
--grampsjs-color-link-hover: #62adf8;
--grampsjs-color-page-loading-progress: #42a6d2;
--grampsjs-color-revision-deleted-background-red: rgba(248, 81, 73, 0.3); --grampsjs-color-revision-deleted-background-red: rgba(248, 81, 73, 0.3);
--grampsjs-color-revision-added-background-green: rgba(46, 160, 67, 0.15); --grampsjs-color-revision-added-background-green: rgba(46, 160, 67, 0.15);
--grampsjs-color-revision-moved-background-blue: rgba(56, 139, 253, 0.1); --grampsjs-color-revision-moved-background-blue: rgba(56, 139, 253, 0.1);
@@ -238,55 +242,7 @@ body {
--grampsjs-color-ytree-default-person-icon-background: var(--md-sys-color-surface-container); --grampsjs-color-ytree-default-person-icon-background: var(--md-sys-color-surface-container);
--grampsjs-note-color: #ccc; --grampsjs-note-color: #ccc;
--grampsjs-map-marker-color: #bb1f11; --grampsjs-map-marker-color: #bb1f11;
--md-sys-color-primary: #8d7168; /* --md-sys-color-* tokens are set dynamically by applyColors() in theme.js */
--md-sys-color-surface-tint: rgb(233, 189, 174);
--md-sys-color-on-primary: rgba(0, 0, 0, 0.8);
--md-sys-color-primary-container: rgb(109, 76, 65);
--md-sys-color-on-primary-container: rgb(235, 190, 176);
--md-sys-color-secondary: rgb(156, 203, 251);
--md-sys-color-on-secondary: rgb(0, 51, 84);
--md-sys-color-secondary-container: rgb(49, 98, 141);
--md-sys-color-on-secondary-container: rgb(189, 220, 255);
--md-sys-color-tertiary: rgb(214, 198, 141);
--md-sys-color-on-tertiary: rgb(57, 48, 5);
--md-sys-color-tertiary-container: rgb(106, 94, 47);
--md-sys-color-on-tertiary-container: rgb(233, 216, 158);
--md-sys-color-error: rgb(255, 180, 171);
--md-sys-color-on-error: rgb(105, 0, 4);
--md-sys-color-error-container: rgb(186, 26, 26);
--md-sys-color-on-error-container: rgb(255, 205, 199);
--md-sys-color-background: rgb(22, 19, 18);
--md-sys-color-on-background: rgb(233, 225, 222);
--md-sys-color-surface: rgb(20, 19, 19);
--md-sys-color-on-surface: rgb(229, 226, 225);
--md-sys-color-surface-variant: rgb(68, 71, 72);
--md-sys-color-on-surface-variant: rgb(196, 199, 200);
--md-sys-color-outline: rgb(142, 145, 146);
--md-sys-color-outline-variant: rgb(68, 71, 72);
--md-sys-color-shadow: rgb(0, 0, 0);
--md-sys-color-scrim: rgb(0, 0, 0);
--md-sys-color-inverse-surface: rgb(229, 226, 225);
--md-sys-color-inverse-on-surface: rgb(49, 48, 48);
--md-sys-color-inverse-primary: rgb(121, 86, 75);
--md-sys-color-primary-fixed: rgb(255, 219, 207);
--md-sys-color-on-primary-fixed: rgb(45, 21, 13);
--md-sys-color-primary-fixed-dim: rgb(233, 189, 174);
--md-sys-color-on-primary-fixed-variant: rgb(94, 63, 53);
--md-sys-color-secondary-fixed: rgb(207, 229, 255);
--md-sys-color-on-secondary-fixed: rgb(0, 29, 51);
--md-sys-color-secondary-fixed-dim: rgb(156, 203, 251);
--md-sys-color-on-secondary-fixed-variant: rgb(17, 74, 115);
--md-sys-color-tertiary-fixed: rgb(243, 226, 167);
--md-sys-color-on-tertiary-fixed: rgb(34, 27, 0);
--md-sys-color-tertiary-fixed-dim: rgb(214, 198, 141);
--md-sys-color-on-tertiary-fixed-variant: rgb(81, 70, 26);
--md-sys-color-surface-dim: rgb(20, 19, 19);
--md-sys-color-surface-bright: rgb(58, 57, 57);
--md-sys-color-surface-container-lowest: rgb(14, 14, 14);
--md-sys-color-surface-container-low: rgb(28, 27, 27);
--md-sys-color-surface-container: rgb(32, 31, 31);
--md-sys-color-surface-container-high: rgb(42, 42, 42);
--md-sys-color-surface-container-highest: rgb(53, 52, 52);
--mdc-select-fill-color: var(--md-sys-color-surface-container-highest); --mdc-select-fill-color: var(--md-sys-color-surface-container-highest);
--mdc-select-ink-color: var(--md-sys-color-on-surface); --mdc-select-ink-color: var(--md-sys-color-on-surface);
--mdc-select-label-ink-color: rgba(255, 255, 255, 0.5); --mdc-select-label-ink-color: rgba(255, 255, 255, 0.5);
+11
View File
@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@awesome.me/webawesome": "^3.4.0", "@awesome.me/webawesome": "^3.4.0",
"@hpcc-js/wasm": "^2.22.4", "@hpcc-js/wasm": "^2.22.4",
"@material/material-color-utilities": "^0.4.0",
"@material/mwc-button": "^0.27.0", "@material/mwc-button": "^0.27.0",
"@material/mwc-circular-progress": "^0.27.0", "@material/mwc-circular-progress": "^0.27.0",
"@material/mwc-drawer": "^0.27.0", "@material/mwc-drawer": "^0.27.0",
@@ -2859,6 +2860,11 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@material/material-color-utilities": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.4.0.tgz",
"integrity": "sha512-dlq6VExJReb8dhjj3a/yTigr3ncNwoFmL5Iy2ENtbDX03EmNeOEdZ+vsaGrj7RTuO+mB7L58II4LCsl4NpM8uw=="
},
"node_modules/@material/menu": { "node_modules/@material/menu": {
"version": "14.0.0-canary.53b3cad2f.0", "version": "14.0.0-canary.53b3cad2f.0",
"resolved": "https://registry.npmjs.org/@material/menu/-/menu-14.0.0-canary.53b3cad2f.0.tgz", "resolved": "https://registry.npmjs.org/@material/menu/-/menu-14.0.0-canary.53b3cad2f.0.tgz",
@@ -16642,6 +16648,11 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"@material/material-color-utilities": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.4.0.tgz",
"integrity": "sha512-dlq6VExJReb8dhjj3a/yTigr3ncNwoFmL5Iy2ENtbDX03EmNeOEdZ+vsaGrj7RTuO+mB7L58II4LCsl4NpM8uw=="
},
"@material/menu": { "@material/menu": {
"version": "14.0.0-canary.53b3cad2f.0", "version": "14.0.0-canary.53b3cad2f.0",
"resolved": "https://registry.npmjs.org/@material/menu/-/menu-14.0.0-canary.53b3cad2f.0.tgz", "resolved": "https://registry.npmjs.org/@material/menu/-/menu-14.0.0-canary.53b3cad2f.0.tgz",
+2 -1
View File
@@ -127,6 +127,7 @@
"dependencies": { "dependencies": {
"@awesome.me/webawesome": "^3.4.0", "@awesome.me/webawesome": "^3.4.0",
"@hpcc-js/wasm": "^2.22.4", "@hpcc-js/wasm": "^2.22.4",
"@material/material-color-utilities": "^0.4.0",
"@material/mwc-button": "^0.27.0", "@material/mwc-button": "^0.27.0",
"@material/mwc-circular-progress": "^0.27.0", "@material/mwc-circular-progress": "^0.27.0",
"@material/mwc-drawer": "^0.27.0", "@material/mwc-drawer": "^0.27.0",
@@ -163,4 +164,4 @@
"pwa-helpers": "^0.9.1", "pwa-helpers": "^0.9.1",
"tippy.js": "^6.3.7" "tippy.js": "^6.3.7"
} }
} }
+11 -3
View File
@@ -39,7 +39,7 @@ import './components/GrampsjsUndoTransaction.js'
import './components/GrampsjsUpdateAvailable.js' import './components/GrampsjsUpdateAvailable.js'
import './components/GrampsjsUpgradeDb.js' import './components/GrampsjsUpgradeDb.js'
import {sharedStyles} from './SharedStyles.js' import {sharedStyles} from './SharedStyles.js'
import {applyTheme} from './theme.js' import {applyScheme, DEFAULT_PRIMARY, DEFAULT_SECONDARY} from './theme.js'
import {handleOIDCCallback, handleOIDCComplete} from './oidc.js' import {handleOIDCCallback, handleOIDCComplete} from './oidc.js'
const LOADING_STATE_INITIAL = 0 const LOADING_STATE_INITIAL = 0
@@ -661,10 +661,18 @@ export class GrampsJs extends LitElement {
this._loadFrontendStrings(this.appState.settings.lang) this._loadFrontendStrings(this.appState.settings.lang)
} }
applyTheme(this.appState.settings.theme) applyScheme(
DEFAULT_PRIMARY,
DEFAULT_SECONDARY,
this.appState.settings.theme
)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', () => mediaQuery.addEventListener('change', () =>
applyTheme(this.appState.settings.theme) applyScheme(
DEFAULT_PRIMARY,
DEFAULT_SECONDARY,
this.appState.settings.theme
)
) )
} }
+109 -2
View File
@@ -1,3 +1,16 @@
import {
DynamicScheme,
Hct,
MaterialDynamicColors,
TonalPalette,
Variant,
argbFromHex,
hexFromArgb,
} from '@material/material-color-utilities'
export const DEFAULT_PRIMARY = '#6d4c41'
export const DEFAULT_SECONDARY = '#0277bd'
export function getSystemTheme() { export function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' ? 'dark'
@@ -8,6 +21,100 @@ export function getCurrentTheme(theme) {
return theme === undefined || theme === 'system' ? getSystemTheme() : theme return theme === undefined || theme === 'system' ? getSystemTheme() : theme
} }
export function applyTheme(theme) { function toKebabCase(str) {
document.documentElement.setAttribute('data-theme', getCurrentTheme(theme)) return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
export function applyColors(
primaryHex = DEFAULT_PRIMARY,
secondaryHex = DEFAULT_SECONDARY,
isDark = false
) {
const primaryHct = Hct.fromInt(argbFromHex(primaryHex))
const secondaryHct = Hct.fromInt(argbFromHex(secondaryHex))
const secondaryPalette = TonalPalette.fromHueAndChroma(
secondaryHct.hue,
secondaryHct.chroma
)
const primaryPalette = TonalPalette.fromHueAndChroma(
primaryHct.hue,
primaryHct.chroma
)
const scheme = new DynamicScheme({
sourceColorHct: primaryHct,
variant: Variant.TONAL_SPOT,
isDark,
contrastLevel: 0,
// Preserve the seed's own hue+chroma rather than TONAL_SPOT's fixed values.
primaryPalette,
secondaryPalette,
// Chroma 0 → pure neutral grays; no hue tint bleeds into surfaces.
neutralPalette: TonalPalette.fromHueAndChroma(primaryHct.hue, 0),
neutralVariantPalette: TonalPalette.fromHueAndChroma(primaryHct.hue, 0),
})
const root = document.documentElement
for (const [name, color] of Object.entries(MaterialDynamicColors)) {
if (typeof color?.getArgb !== 'function') continue
root.style.setProperty(
`--md-sys-color-${toKebabCase(name)}`,
hexFromArgb(color.getArgb(scheme))
)
}
// Primary: use seed's own tone clamped to [30, 50] so users see their chosen
// colour on the app bar. Tone 30-50 always reads white on-primary text.
const primaryTone = isDark
? Math.min(80, Math.max(60, Math.round(primaryHct.tone)))
: Math.min(50, Math.max(30, Math.round(primaryHct.tone)))
root.style.setProperty(
'--md-sys-color-primary',
hexFromArgb(primaryPalette.tone(primaryTone))
)
// M3 achromatic tones 90-98 are darker than expected for a web app.
// Override light-mode surfaces to lighter neutrals.
if (!isDark) {
root.style.setProperty('--md-sys-color-background', '#ffffff')
root.style.setProperty('--md-sys-color-surface', '#ffffff')
// lowest/low/container → menus, cards, sheets: white
root.style.setProperty('--md-sys-color-surface-container-lowest', '#ffffff')
root.style.setProperty('--md-sys-color-surface-container-low', '#ffffff')
root.style.setProperty('--md-sys-color-surface-container', '#ffffff')
// high/highest → filled input and select backgrounds: light gray
root.style.setProperty(
'--md-sys-color-surface-container-high',
'rgb(240, 240, 240)'
)
root.style.setProperty(
'--md-sys-color-surface-container-highest',
'rgb(235, 235, 235)'
)
}
// Action color for edit/add/delete buttons: clamp tone to a readable range
// so very light or very dark seeds still contrast against their background.
const actionTone = isDark
? Math.min(80, Math.max(60, Math.round(secondaryHct.tone)))
: Math.min(55, Math.max(30, Math.round(secondaryHct.tone)))
root.style.setProperty(
'--mdc-theme-secondary',
hexFromArgb(secondaryPalette.tone(actionTone))
)
root.style.setProperty(
'--mdc-theme-on-secondary',
actionTone < 60 ? 'rgba(255, 255, 255, 0.95)' : 'rgba(0, 0, 0, 0.87)'
)
}
export function applyScheme(
primaryHex = DEFAULT_PRIMARY,
secondaryHex = DEFAULT_SECONDARY,
theme
) {
const isDark = getCurrentTheme(theme) === 'dark'
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light')
applyColors(primaryHex, secondaryHex, isDark)
} }
+2 -2
View File
@@ -18,7 +18,7 @@ import {GrampsjsView} from './GrampsjsView.js'
import {mdiCheck, mdiContentCopy} from '@mdi/js' import {mdiCheck, mdiContentCopy} from '@mdi/js'
import {__APIHOST__} from '../api.js' import {__APIHOST__} from '../api.js'
import {fireEvent} from '../util.js' import {fireEvent} from '../util.js'
import {applyTheme} from '../theme.js' import {applyScheme, DEFAULT_PRIMARY, DEFAULT_SECONDARY} from '../theme.js'
import {DEFAULT_TREE_VIEW, TREE_VIEWS} from '../treeDefaults.js' import {DEFAULT_TREE_VIEW, TREE_VIEWS} from '../treeDefaults.js'
export class GrampsjsViewSettingsUser extends GrampsjsView { export class GrampsjsViewSettingsUser extends GrampsjsView {
@@ -186,7 +186,7 @@ export class GrampsjsViewSettingsUser extends GrampsjsView {
_handleThemeSelected(event) { _handleThemeSelected(event) {
const theme = event.target.value const theme = event.target.value
this.appState.updateSettings({theme}) this.appState.updateSettings({theme})
applyTheme(theme) applyScheme(DEFAULT_PRIMARY, DEFAULT_SECONDARY, theme)
} }
renderTreePreferences() { renderTreePreferences() {
+7
View File
@@ -8,5 +8,12 @@ export default defineConfig({
reporter: ['text', 'lcov'], reporter: ['text', 'lcov'],
include: ['src/**/*.js'], include: ['src/**/*.js'],
}, },
server: {
deps: {
// ESM imports in this package lack .js extensions, which Node.js
// strict ESM requires. Inline through Vite's bundler instead.
inline: ['@material/material-color-utilities'],
},
},
}, },
}) })