@labpics/colors 0.1.0 → 0.3.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.
- package/README.md +248 -242
- package/adapt-theme.d.ts +67 -67
- package/adapt-theme.js +431 -431
- package/apply-theme.d.ts +16 -16
- package/apply-theme.js +44 -44
- package/effective-bg.d.ts +48 -48
- package/effective-bg.js +255 -255
- package/index.d.ts +36 -36
- package/index.js +21 -21
- package/package.json +65 -65
- package/pkg/labcolors.js +5 -5
- package/pkg/labcolors_bg.wasm +0 -0
- package/watch-theme.d.ts +57 -57
- package/watch-theme.js +144 -144
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@labpics/colors",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Framework-agnostic contrast engine: resolve accessible, perceptually-anchored colour roles for any background. WASM core, zero runtime dependencies.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public",
|
|
9
|
-
"provenance":
|
|
10
|
-
},
|
|
11
|
-
"repository": {
|
|
12
|
-
"type": "git",
|
|
13
|
-
"url": "https://github.com/Labpics-Team/lab-colors",
|
|
14
|
-
"directory": "packages/colors"
|
|
15
|
-
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": {
|
|
18
|
-
"types": "./index.d.ts",
|
|
19
|
-
"default": "./index.js"
|
|
20
|
-
},
|
|
21
|
-
"./apply-theme": {
|
|
22
|
-
"types": "./apply-theme.d.ts",
|
|
23
|
-
"default": "./apply-theme.js"
|
|
24
|
-
},
|
|
25
|
-
"./watch-theme": {
|
|
26
|
-
"types": "./watch-theme.d.ts",
|
|
27
|
-
"default": "./watch-theme.js"
|
|
28
|
-
},
|
|
29
|
-
"./adapt-theme": {
|
|
30
|
-
"types": "./adapt-theme.d.ts",
|
|
31
|
-
"default": "./adapt-theme.js"
|
|
32
|
-
},
|
|
33
|
-
"./effective-bg": {
|
|
34
|
-
"types": "./effective-bg.d.ts",
|
|
35
|
-
"default": "./effective-bg.js"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"files": [
|
|
39
|
-
"index.js",
|
|
40
|
-
"index.d.ts",
|
|
41
|
-
"apply-theme.js",
|
|
42
|
-
"apply-theme.d.ts",
|
|
43
|
-
"watch-theme.js",
|
|
44
|
-
"watch-theme.d.ts",
|
|
45
|
-
"adapt-theme.js",
|
|
46
|
-
"adapt-theme.d.ts",
|
|
47
|
-
"effective-bg.js",
|
|
48
|
-
"effective-bg.d.ts",
|
|
49
|
-
"pkg/labcolors.js",
|
|
50
|
-
"pkg/labcolors.d.ts",
|
|
51
|
-
"pkg/labcolors_bg.wasm",
|
|
52
|
-
"pkg/labcolors_bg.wasm.d.ts"
|
|
53
|
-
],
|
|
54
|
-
"scripts": {
|
|
55
|
-
"//build": "wasm-pack resolves --out-dir relative to the CRATE dir (crates/labcolors-wasm), not the cwd, so the ../../packages path is correct and unambiguous regardless of where npm runs it.",
|
|
56
|
-
"build": "wasm-pack build ../../crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors",
|
|
57
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
58
|
-
"test": "node --test"
|
|
59
|
-
},
|
|
60
|
-
"devDependencies": {
|
|
61
|
-
"typescript": "^5.5.0"
|
|
62
|
-
},
|
|
63
|
-
"types": "./index.d.ts",
|
|
64
|
-
"sideEffects": false
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@labpics/colors",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Framework-agnostic contrast engine: resolve accessible, perceptually-anchored colour roles for any background. WASM core, zero runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"provenance": false
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/Labpics-Team/lab-colors",
|
|
14
|
+
"directory": "packages/colors"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"default": "./index.js"
|
|
20
|
+
},
|
|
21
|
+
"./apply-theme": {
|
|
22
|
+
"types": "./apply-theme.d.ts",
|
|
23
|
+
"default": "./apply-theme.js"
|
|
24
|
+
},
|
|
25
|
+
"./watch-theme": {
|
|
26
|
+
"types": "./watch-theme.d.ts",
|
|
27
|
+
"default": "./watch-theme.js"
|
|
28
|
+
},
|
|
29
|
+
"./adapt-theme": {
|
|
30
|
+
"types": "./adapt-theme.d.ts",
|
|
31
|
+
"default": "./adapt-theme.js"
|
|
32
|
+
},
|
|
33
|
+
"./effective-bg": {
|
|
34
|
+
"types": "./effective-bg.d.ts",
|
|
35
|
+
"default": "./effective-bg.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"index.js",
|
|
40
|
+
"index.d.ts",
|
|
41
|
+
"apply-theme.js",
|
|
42
|
+
"apply-theme.d.ts",
|
|
43
|
+
"watch-theme.js",
|
|
44
|
+
"watch-theme.d.ts",
|
|
45
|
+
"adapt-theme.js",
|
|
46
|
+
"adapt-theme.d.ts",
|
|
47
|
+
"effective-bg.js",
|
|
48
|
+
"effective-bg.d.ts",
|
|
49
|
+
"pkg/labcolors.js",
|
|
50
|
+
"pkg/labcolors.d.ts",
|
|
51
|
+
"pkg/labcolors_bg.wasm",
|
|
52
|
+
"pkg/labcolors_bg.wasm.d.ts"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"//build": "wasm-pack resolves --out-dir relative to the CRATE dir (crates/labcolors-wasm), not the cwd, so the ../../packages path is correct and unambiguous regardless of where npm runs it.",
|
|
56
|
+
"build": "wasm-pack build ../../crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors",
|
|
57
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
58
|
+
"test": "node --test"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"typescript": "^5.5.0"
|
|
62
|
+
},
|
|
63
|
+
"types": "./index.d.ts",
|
|
64
|
+
"sideEffects": false
|
|
65
|
+
}
|
package/pkg/labcolors.js
CHANGED
|
@@ -191,11 +191,11 @@ if (Symbol.dispose) LabColors.prototype[Symbol.dispose] = LabColors.prototype.fr
|
|
|
191
191
|
function __wbg_get_imports() {
|
|
192
192
|
const import0 = {
|
|
193
193
|
__proto__: null,
|
|
194
|
-
|
|
194
|
+
__wbg_Error_92b29b0548f8b746: function(arg0, arg1) {
|
|
195
195
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
196
196
|
return addHeapObject(ret);
|
|
197
197
|
},
|
|
198
|
-
|
|
198
|
+
__wbg___wbindgen_string_get_b0ca35b86a603356: function(arg0, arg1) {
|
|
199
199
|
const obj = getObject(arg1);
|
|
200
200
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
201
201
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
@@ -203,14 +203,14 @@ function __wbg_get_imports() {
|
|
|
203
203
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
204
204
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
205
205
|
},
|
|
206
|
-
|
|
206
|
+
__wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
|
|
207
207
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
208
208
|
},
|
|
209
|
-
|
|
209
|
+
__wbg_new_da52cf8fe3429cb2: function() {
|
|
210
210
|
const ret = new Object();
|
|
211
211
|
return addHeapObject(ret);
|
|
212
212
|
},
|
|
213
|
-
|
|
213
|
+
__wbg_set_8535240470bf2500: function() { return handleError(function (arg0, arg1, arg2) {
|
|
214
214
|
const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2));
|
|
215
215
|
return ret;
|
|
216
216
|
}, arguments); },
|
package/pkg/labcolors_bg.wasm
CHANGED
|
Binary file
|
package/watch-theme.d.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
// Public types for the reactive theme runtime.
|
|
2
|
-
|
|
3
|
-
import type { LabColors, ResolvedTheme, ThemeName } from "./pkg/labcolors.js";
|
|
4
|
-
|
|
5
|
-
export interface WatchThemeOptions {
|
|
6
|
-
/** An initialised `LabColors` engine (after `await init()`). */
|
|
7
|
-
colors: Pick<LabColors, "resolveTheme">;
|
|
8
|
-
/** Theme name. */
|
|
9
|
-
theme: ThemeName;
|
|
10
|
-
/**
|
|
11
|
-
* Explicit effective background, overriding the ancestor-composite. Use a hex
|
|
12
|
-
* string or a `() => hex` you sample yourself for image/gradient/blur surfaces
|
|
13
|
-
* that have no single readable `background-color`.
|
|
14
|
-
*/
|
|
15
|
-
background?: string | (() => string);
|
|
16
|
-
/** Element to write the `--lab-*` variables onto. Defaults to the watched element. */
|
|
17
|
-
target?: HTMLElement;
|
|
18
|
-
/** Base colour when the ancestor chain is fully translucent. Default `"#FFFFFF"`. */
|
|
19
|
-
fallback?: string;
|
|
20
|
-
/** Auto-refresh on DOM attribute mutations. Default `true`. */
|
|
21
|
-
observe?: boolean;
|
|
22
|
-
/** Mutation-observer root. Defaults to the document element. */
|
|
23
|
-
root?: Node;
|
|
24
|
-
/** Window-like host (for `MutationObserver`). Defaults to `globalThis`. */
|
|
25
|
-
win?: Window;
|
|
26
|
-
/** Injection seam for the computed style of an element (testing). */
|
|
27
|
-
getStyle?: (element: unknown) => { getPropertyValue(property: string): string };
|
|
28
|
-
/** Injection seam for an element's parent (testing). */
|
|
29
|
-
parentOf?: (element: unknown) => unknown;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface WatchController {
|
|
33
|
-
/**
|
|
34
|
-
* Re-resolve and re-apply if the effective background (or theme) changed;
|
|
35
|
-
* `force` re-applies unconditionally. Returns the now-applied result, or the
|
|
36
|
-
* cached one when nothing changed.
|
|
37
|
-
*/
|
|
38
|
-
refresh(force?: boolean): ResolvedTheme | null;
|
|
39
|
-
/** Switch theme and re-apply. */
|
|
40
|
-
setTheme(theme: ThemeName): void;
|
|
41
|
-
/** The effective background hex last resolved. */
|
|
42
|
-
background(): string;
|
|
43
|
-
/** Disconnect observers and stop watching. */
|
|
44
|
-
stop(): void;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Keep an element's `--lab-*` variables in sync with its (effective) background.
|
|
49
|
-
*
|
|
50
|
-
* Discrete background/theme changes are caught by a `MutationObserver`;
|
|
51
|
-
* continuous (animated) backgrounds are driven by calling `refresh()` from a
|
|
52
|
-
* `requestAnimationFrame` loop (cheap — re-resolves only on real change).
|
|
53
|
-
*/
|
|
54
|
-
export declare function watchTheme(
|
|
55
|
-
element: HTMLElement,
|
|
56
|
-
options: WatchThemeOptions,
|
|
57
|
-
): WatchController;
|
|
1
|
+
// Public types for the reactive theme runtime.
|
|
2
|
+
|
|
3
|
+
import type { LabColors, ResolvedTheme, ThemeName } from "./pkg/labcolors.js";
|
|
4
|
+
|
|
5
|
+
export interface WatchThemeOptions {
|
|
6
|
+
/** An initialised `LabColors` engine (after `await init()`). */
|
|
7
|
+
colors: Pick<LabColors, "resolveTheme">;
|
|
8
|
+
/** Theme name. */
|
|
9
|
+
theme: ThemeName;
|
|
10
|
+
/**
|
|
11
|
+
* Explicit effective background, overriding the ancestor-composite. Use a hex
|
|
12
|
+
* string or a `() => hex` you sample yourself for image/gradient/blur surfaces
|
|
13
|
+
* that have no single readable `background-color`.
|
|
14
|
+
*/
|
|
15
|
+
background?: string | (() => string);
|
|
16
|
+
/** Element to write the `--lab-*` variables onto. Defaults to the watched element. */
|
|
17
|
+
target?: HTMLElement;
|
|
18
|
+
/** Base colour when the ancestor chain is fully translucent. Default `"#FFFFFF"`. */
|
|
19
|
+
fallback?: string;
|
|
20
|
+
/** Auto-refresh on DOM attribute mutations. Default `true`. */
|
|
21
|
+
observe?: boolean;
|
|
22
|
+
/** Mutation-observer root. Defaults to the document element. */
|
|
23
|
+
root?: Node;
|
|
24
|
+
/** Window-like host (for `MutationObserver`). Defaults to `globalThis`. */
|
|
25
|
+
win?: Window;
|
|
26
|
+
/** Injection seam for the computed style of an element (testing). */
|
|
27
|
+
getStyle?: (element: unknown) => { getPropertyValue(property: string): string };
|
|
28
|
+
/** Injection seam for an element's parent (testing). */
|
|
29
|
+
parentOf?: (element: unknown) => unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WatchController {
|
|
33
|
+
/**
|
|
34
|
+
* Re-resolve and re-apply if the effective background (or theme) changed;
|
|
35
|
+
* `force` re-applies unconditionally. Returns the now-applied result, or the
|
|
36
|
+
* cached one when nothing changed.
|
|
37
|
+
*/
|
|
38
|
+
refresh(force?: boolean): ResolvedTheme | null;
|
|
39
|
+
/** Switch theme and re-apply. */
|
|
40
|
+
setTheme(theme: ThemeName): void;
|
|
41
|
+
/** The effective background hex last resolved. */
|
|
42
|
+
background(): string;
|
|
43
|
+
/** Disconnect observers and stop watching. */
|
|
44
|
+
stop(): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keep an element's `--lab-*` variables in sync with its (effective) background.
|
|
49
|
+
*
|
|
50
|
+
* Discrete background/theme changes are caught by a `MutationObserver`;
|
|
51
|
+
* continuous (animated) backgrounds are driven by calling `refresh()` from a
|
|
52
|
+
* `requestAnimationFrame` loop (cheap — re-resolves only on real change).
|
|
53
|
+
*/
|
|
54
|
+
export declare function watchTheme(
|
|
55
|
+
element: HTMLElement,
|
|
56
|
+
options: WatchThemeOptions,
|
|
57
|
+
): WatchController;
|
package/watch-theme.js
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
// Reactive theme runtime — zero dependencies.
|
|
2
|
-
//
|
|
3
|
-
// `applyTheme` writes a resolved theme's `--lab-*` variables onto an element
|
|
4
|
-
// once. `watchTheme` closes the loop the css-injection-runtime chapter left
|
|
5
|
-
// open: it keeps an element's variables *in sync* with its background. Point it
|
|
6
|
-
// at a surface and theming follows the background — the seamless drop-in for a
|
|
7
|
-
// design system that re-resolves against a changing background.
|
|
8
|
-
//
|
|
9
|
-
// It serves both regimes:
|
|
10
|
-
// * DISCRETE changes (theme switch, a class/style toggle, DOM reflow) are
|
|
11
|
-
// caught automatically by a `MutationObserver` — call once, forget.
|
|
12
|
-
// * CONTINUOUS changes (a CSS-animated or per-frame-scripted background that
|
|
13
|
-
// never mutates inline style) are driven by the caller calling `refresh()`
|
|
14
|
-
// inside its own `requestAnimationFrame` loop. `refresh()` is cheap: it
|
|
15
|
-
// re-resolves only when the effective background actually changed, so a
|
|
16
|
-
// steady background costs one string compare per frame, not a WASM solve.
|
|
17
|
-
//
|
|
18
|
-
// The effective background is computed by alpha-compositing ancestors
|
|
19
|
-
// (`effective-bg.js`); for surfaces over images/gradients/blur — which have no
|
|
20
|
-
// single readable colour — pass an explicit `background` (a hex string or a
|
|
21
|
-
// `() => hex` you sample yourself).
|
|
22
|
-
|
|
23
|
-
import { applyTheme } from "./apply-theme.js";
|
|
24
|
-
import { effectiveBackground } from "./effective-bg.js";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {object} WatchController
|
|
28
|
-
* @property {(force?: boolean) => (object | null)} refresh Re-resolve+apply if the
|
|
29
|
-
* background (or theme) changed; `force` re-applies unconditionally. Returns the
|
|
30
|
-
* `resolveTheme` result that is now applied, or `null` if nothing was applied.
|
|
31
|
-
* @property {(theme: string) => void} setTheme Switch theme and re-apply.
|
|
32
|
-
* @property {() => string} background The effective background hex last resolved.
|
|
33
|
-
* @property {() => void} stop Disconnect observers and stop watching.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Keep `element`'s `--lab-*` variables in sync with its (effective) background.
|
|
38
|
-
*
|
|
39
|
-
* @param {*} element The surface to read the background from and (by default)
|
|
40
|
-
* write the variables onto.
|
|
41
|
-
* @param {object} options
|
|
42
|
-
* @param {{ resolveTheme: (bgHex: string, theme: string) => object }} options.colors
|
|
43
|
-
* An initialised `LabColors` engine (already `await init()`-ed).
|
|
44
|
-
* @param {string} options.theme Theme name (`"light" | "dark" | …`).
|
|
45
|
-
* @param {string | (() => string)} [options.background] Explicit effective
|
|
46
|
-
* background, overriding the ancestor-composite (use for image/gradient/blur
|
|
47
|
-
* surfaces you sample yourself).
|
|
48
|
-
* @param {*} [options.target=element] Element to write the variables onto.
|
|
49
|
-
* @param {string} [options.fallback="#FFFFFF"] Base for a fully-translucent chain.
|
|
50
|
-
* @param {boolean} [options.observe=true] Auto-refresh on DOM attribute mutations.
|
|
51
|
-
* @param {*} [options.root] Mutation-observer root (default: the document element).
|
|
52
|
-
* @param {*} [options.win=globalThis] Window-like host (for MutationObserver).
|
|
53
|
-
* @param {(el:*)=>*} [options.getStyle] Injection seam for `effectiveBackground`.
|
|
54
|
-
* @param {(el:*)=>*} [options.parentOf] Injection seam for `effectiveBackground`.
|
|
55
|
-
* @returns {WatchController}
|
|
56
|
-
*/
|
|
57
|
-
export function watchTheme(element, options) {
|
|
58
|
-
if (!options || typeof options.colors?.resolveTheme !== "function") {
|
|
59
|
-
throw new TypeError("watchTheme: options.colors must be an initialised LabColors engine");
|
|
60
|
-
}
|
|
61
|
-
if (typeof options.theme !== "string") {
|
|
62
|
-
throw new TypeError("watchTheme: options.theme must be a theme name string");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const target = options.target ?? element;
|
|
66
|
-
const fallback = options.fallback ?? "#FFFFFF";
|
|
67
|
-
const win = options.win ?? (typeof globalThis !== "undefined" ? globalThis : undefined);
|
|
68
|
-
let theme = options.theme;
|
|
69
|
-
let lastBg = null;
|
|
70
|
-
let lastTheme = null;
|
|
71
|
-
let lastResult = null;
|
|
72
|
-
|
|
73
|
-
const readBackground = () => {
|
|
74
|
-
const b = options.background;
|
|
75
|
-
if (typeof b === "function") return b();
|
|
76
|
-
if (typeof b === "string") return b;
|
|
77
|
-
return effectiveBackground(element, {
|
|
78
|
-
fallback,
|
|
79
|
-
getStyle: options.getStyle,
|
|
80
|
-
parentOf: options.parentOf,
|
|
81
|
-
});
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const refresh = (force = false) => {
|
|
85
|
-
const bg = readBackground();
|
|
86
|
-
if (!force && bg === lastBg && theme === lastTheme) return lastResult;
|
|
87
|
-
const result = options.colors.resolveTheme(bg, theme);
|
|
88
|
-
applyTheme(target, result);
|
|
89
|
-
lastBg = bg;
|
|
90
|
-
lastTheme = theme;
|
|
91
|
-
lastResult = result;
|
|
92
|
-
return result;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Coalesce a burst of mutations into a single refresh on the next microtask.
|
|
96
|
-
let scheduled = false;
|
|
97
|
-
let stopped = false;
|
|
98
|
-
const schedule = () => {
|
|
99
|
-
if (scheduled || stopped) return;
|
|
100
|
-
scheduled = true;
|
|
101
|
-
Promise.resolve().then(() => {
|
|
102
|
-
scheduled = false;
|
|
103
|
-
// A `stop()` between scheduling and this microtask must cancel the refresh
|
|
104
|
-
// — the watcher is done, no late writes.
|
|
105
|
-
if (!stopped) refresh();
|
|
106
|
-
});
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
let observer = null;
|
|
110
|
-
if (options.observe !== false && win && typeof win.MutationObserver === "function") {
|
|
111
|
-
const root =
|
|
112
|
-
options.root ??
|
|
113
|
-
(typeof win.document !== "undefined" ? win.document.documentElement : null);
|
|
114
|
-
if (root) {
|
|
115
|
-
observer = new win.MutationObserver(schedule);
|
|
116
|
-
// A background can change on the element OR any ancestor, via inline style
|
|
117
|
-
// or a class swap — so watch attribute changes across the subtree.
|
|
118
|
-
observer.observe(root, {
|
|
119
|
-
subtree: true,
|
|
120
|
-
attributes: true,
|
|
121
|
-
attributeFilter: ["style", "class"],
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Apply immediately so the surface is correct on creation.
|
|
127
|
-
refresh(true);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
refresh,
|
|
131
|
-
setTheme(next) {
|
|
132
|
-
theme = next;
|
|
133
|
-
refresh();
|
|
134
|
-
},
|
|
135
|
-
background() {
|
|
136
|
-
return lastBg;
|
|
137
|
-
},
|
|
138
|
-
stop() {
|
|
139
|
-
stopped = true;
|
|
140
|
-
if (observer) observer.disconnect();
|
|
141
|
-
observer = null;
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|
|
1
|
+
// Reactive theme runtime — zero dependencies.
|
|
2
|
+
//
|
|
3
|
+
// `applyTheme` writes a resolved theme's `--lab-*` variables onto an element
|
|
4
|
+
// once. `watchTheme` closes the loop the css-injection-runtime chapter left
|
|
5
|
+
// open: it keeps an element's variables *in sync* with its background. Point it
|
|
6
|
+
// at a surface and theming follows the background — the seamless drop-in for a
|
|
7
|
+
// design system that re-resolves against a changing background.
|
|
8
|
+
//
|
|
9
|
+
// It serves both regimes:
|
|
10
|
+
// * DISCRETE changes (theme switch, a class/style toggle, DOM reflow) are
|
|
11
|
+
// caught automatically by a `MutationObserver` — call once, forget.
|
|
12
|
+
// * CONTINUOUS changes (a CSS-animated or per-frame-scripted background that
|
|
13
|
+
// never mutates inline style) are driven by the caller calling `refresh()`
|
|
14
|
+
// inside its own `requestAnimationFrame` loop. `refresh()` is cheap: it
|
|
15
|
+
// re-resolves only when the effective background actually changed, so a
|
|
16
|
+
// steady background costs one string compare per frame, not a WASM solve.
|
|
17
|
+
//
|
|
18
|
+
// The effective background is computed by alpha-compositing ancestors
|
|
19
|
+
// (`effective-bg.js`); for surfaces over images/gradients/blur — which have no
|
|
20
|
+
// single readable colour — pass an explicit `background` (a hex string or a
|
|
21
|
+
// `() => hex` you sample yourself).
|
|
22
|
+
|
|
23
|
+
import { applyTheme } from "./apply-theme.js";
|
|
24
|
+
import { effectiveBackground } from "./effective-bg.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} WatchController
|
|
28
|
+
* @property {(force?: boolean) => (object | null)} refresh Re-resolve+apply if the
|
|
29
|
+
* background (or theme) changed; `force` re-applies unconditionally. Returns the
|
|
30
|
+
* `resolveTheme` result that is now applied, or `null` if nothing was applied.
|
|
31
|
+
* @property {(theme: string) => void} setTheme Switch theme and re-apply.
|
|
32
|
+
* @property {() => string} background The effective background hex last resolved.
|
|
33
|
+
* @property {() => void} stop Disconnect observers and stop watching.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Keep `element`'s `--lab-*` variables in sync with its (effective) background.
|
|
38
|
+
*
|
|
39
|
+
* @param {*} element The surface to read the background from and (by default)
|
|
40
|
+
* write the variables onto.
|
|
41
|
+
* @param {object} options
|
|
42
|
+
* @param {{ resolveTheme: (bgHex: string, theme: string) => object }} options.colors
|
|
43
|
+
* An initialised `LabColors` engine (already `await init()`-ed).
|
|
44
|
+
* @param {string} options.theme Theme name (`"light" | "dark" | …`).
|
|
45
|
+
* @param {string | (() => string)} [options.background] Explicit effective
|
|
46
|
+
* background, overriding the ancestor-composite (use for image/gradient/blur
|
|
47
|
+
* surfaces you sample yourself).
|
|
48
|
+
* @param {*} [options.target=element] Element to write the variables onto.
|
|
49
|
+
* @param {string} [options.fallback="#FFFFFF"] Base for a fully-translucent chain.
|
|
50
|
+
* @param {boolean} [options.observe=true] Auto-refresh on DOM attribute mutations.
|
|
51
|
+
* @param {*} [options.root] Mutation-observer root (default: the document element).
|
|
52
|
+
* @param {*} [options.win=globalThis] Window-like host (for MutationObserver).
|
|
53
|
+
* @param {(el:*)=>*} [options.getStyle] Injection seam for `effectiveBackground`.
|
|
54
|
+
* @param {(el:*)=>*} [options.parentOf] Injection seam for `effectiveBackground`.
|
|
55
|
+
* @returns {WatchController}
|
|
56
|
+
*/
|
|
57
|
+
export function watchTheme(element, options) {
|
|
58
|
+
if (!options || typeof options.colors?.resolveTheme !== "function") {
|
|
59
|
+
throw new TypeError("watchTheme: options.colors must be an initialised LabColors engine");
|
|
60
|
+
}
|
|
61
|
+
if (typeof options.theme !== "string") {
|
|
62
|
+
throw new TypeError("watchTheme: options.theme must be a theme name string");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const target = options.target ?? element;
|
|
66
|
+
const fallback = options.fallback ?? "#FFFFFF";
|
|
67
|
+
const win = options.win ?? (typeof globalThis !== "undefined" ? globalThis : undefined);
|
|
68
|
+
let theme = options.theme;
|
|
69
|
+
let lastBg = null;
|
|
70
|
+
let lastTheme = null;
|
|
71
|
+
let lastResult = null;
|
|
72
|
+
|
|
73
|
+
const readBackground = () => {
|
|
74
|
+
const b = options.background;
|
|
75
|
+
if (typeof b === "function") return b();
|
|
76
|
+
if (typeof b === "string") return b;
|
|
77
|
+
return effectiveBackground(element, {
|
|
78
|
+
fallback,
|
|
79
|
+
getStyle: options.getStyle,
|
|
80
|
+
parentOf: options.parentOf,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const refresh = (force = false) => {
|
|
85
|
+
const bg = readBackground();
|
|
86
|
+
if (!force && bg === lastBg && theme === lastTheme) return lastResult;
|
|
87
|
+
const result = options.colors.resolveTheme(bg, theme);
|
|
88
|
+
applyTheme(target, result);
|
|
89
|
+
lastBg = bg;
|
|
90
|
+
lastTheme = theme;
|
|
91
|
+
lastResult = result;
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Coalesce a burst of mutations into a single refresh on the next microtask.
|
|
96
|
+
let scheduled = false;
|
|
97
|
+
let stopped = false;
|
|
98
|
+
const schedule = () => {
|
|
99
|
+
if (scheduled || stopped) return;
|
|
100
|
+
scheduled = true;
|
|
101
|
+
Promise.resolve().then(() => {
|
|
102
|
+
scheduled = false;
|
|
103
|
+
// A `stop()` between scheduling and this microtask must cancel the refresh
|
|
104
|
+
// — the watcher is done, no late writes.
|
|
105
|
+
if (!stopped) refresh();
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let observer = null;
|
|
110
|
+
if (options.observe !== false && win && typeof win.MutationObserver === "function") {
|
|
111
|
+
const root =
|
|
112
|
+
options.root ??
|
|
113
|
+
(typeof win.document !== "undefined" ? win.document.documentElement : null);
|
|
114
|
+
if (root) {
|
|
115
|
+
observer = new win.MutationObserver(schedule);
|
|
116
|
+
// A background can change on the element OR any ancestor, via inline style
|
|
117
|
+
// or a class swap — so watch attribute changes across the subtree.
|
|
118
|
+
observer.observe(root, {
|
|
119
|
+
subtree: true,
|
|
120
|
+
attributes: true,
|
|
121
|
+
attributeFilter: ["style", "class"],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Apply immediately so the surface is correct on creation.
|
|
127
|
+
refresh(true);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
refresh,
|
|
131
|
+
setTheme(next) {
|
|
132
|
+
theme = next;
|
|
133
|
+
refresh();
|
|
134
|
+
},
|
|
135
|
+
background() {
|
|
136
|
+
return lastBg;
|
|
137
|
+
},
|
|
138
|
+
stop() {
|
|
139
|
+
stopped = true;
|
|
140
|
+
if (observer) observer.disconnect();
|
|
141
|
+
observer = null;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|