@labpics/colors 0.1.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 +242 -0
- package/adapt-theme.d.ts +67 -0
- package/adapt-theme.js +431 -0
- package/apply-theme.d.ts +16 -0
- package/apply-theme.js +44 -0
- package/effective-bg.d.ts +48 -0
- package/effective-bg.js +255 -0
- package/index.d.ts +36 -0
- package/index.js +21 -0
- package/package.json +65 -0
- package/pkg/labcolors.d.ts +239 -0
- package/pkg/labcolors.js +482 -0
- package/pkg/labcolors_bg.wasm +0 -0
- package/pkg/labcolors_bg.wasm.d.ts +15 -0
- package/watch-theme.d.ts +57 -0
- package/watch-theme.js +144 -0
package/watch-theme.js
ADDED
|
@@ -0,0 +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
|
+
}
|