@celestia-island/hikari 0.55.25 → 0.55.27
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/package.json +1 -1
- package/src/theme/useTheme.test.ts +36 -4
- package/src/theme/useTheme.ts +14 -2
package/package.json
CHANGED
|
@@ -114,13 +114,15 @@ describe("useTheme lean cssvar injection", () => {
|
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
it("writes theme vars into a managed :root style block, never inline", () => {
|
|
117
|
+
it("writes theme vars into a managed html:root style block, never inline", () => {
|
|
118
118
|
theme.initTheme();
|
|
119
119
|
const styleEl = document.head.querySelector("style[data-hikari-theme-vars]");
|
|
120
120
|
expect(styleEl).not.toBeNull();
|
|
121
121
|
// Real browsers compute the static defaults, so the block holds only the
|
|
122
|
-
// true deltas; either way it is
|
|
123
|
-
|
|
122
|
+
// true deltas; either way it is an html:root block (specificity 0,1,1 —
|
|
123
|
+
// it must outrank every :root-level static seed regardless of document
|
|
124
|
+
// order), not an inline attribute.
|
|
125
|
+
expect(styleEl!.textContent).toMatch(/^html:root\{/);
|
|
124
126
|
expect(styleEl!.textContent).toContain("--color-primary");
|
|
125
127
|
// The html inline style attribute stays clean — no token vars on it.
|
|
126
128
|
expect(document.documentElement.style.getPropertyValue("--color-primary")).toBe("");
|
|
@@ -133,9 +135,39 @@ describe("useTheme lean cssvar injection", () => {
|
|
|
133
135
|
theme.useTheme().setTheme("nord");
|
|
134
136
|
const blocks = document.head.querySelectorAll("style[data-hikari-theme-vars]");
|
|
135
137
|
expect(blocks).toHaveLength(1);
|
|
136
|
-
expect(blocks[0].textContent).toMatch(
|
|
138
|
+
expect(blocks[0].textContent).toMatch(/^html:root\{/);
|
|
137
139
|
expect(blocks[0].textContent).not.toBe(before);
|
|
138
140
|
});
|
|
141
|
+
|
|
142
|
+
it("a :root seed injected AFTER the managed block cannot un-theme the page", () => {
|
|
143
|
+
// The 2026-09-18 dev.cw incident: every hikari component sheet @uses
|
|
144
|
+
// tokens.scss, so a lazily-loaded route chunk re-emits the static seed
|
|
145
|
+
// as a `:root` rule appended to <head> after this block — same
|
|
146
|
+
// specificity, later in the cascade, and the brand palette lost to it
|
|
147
|
+
// on the login page. The managed block therefore carries html:root
|
|
148
|
+
// (0,1,1), which outranks any :root-level emission (0,1,0) regardless
|
|
149
|
+
// of document order.
|
|
150
|
+
theme.initTheme();
|
|
151
|
+
const styleEl = document.head.querySelector("style[data-hikari-theme-vars]")!;
|
|
152
|
+
expect(styleEl.textContent).toContain("--color-primary");
|
|
153
|
+
|
|
154
|
+
const computed = () =>
|
|
155
|
+
getComputedStyle(document.documentElement).getPropertyValue("--color-primary").trim();
|
|
156
|
+
const themed = computed();
|
|
157
|
+
expect(themed).not.toBe("");
|
|
158
|
+
|
|
159
|
+
// Simulate the route-chunk seed: a :root rule with the static default,
|
|
160
|
+
// appended LAST (exactly what a lazy chunk's <link> does).
|
|
161
|
+
const seed = document.createElement("style");
|
|
162
|
+
seed.textContent = ":root{--color-primary:122 162 247;--color-background:214 236 240;--color-surface:240 244 248;}";
|
|
163
|
+
document.head.appendChild(seed);
|
|
164
|
+
expect(computed()).toBe(themed);
|
|
165
|
+
|
|
166
|
+
// Re-applying the theme must keep winning, too (no one-shot luck).
|
|
167
|
+
theme.useTheme().setTheme("nord");
|
|
168
|
+
expect(computed()).not.toBe("122 162 247");
|
|
169
|
+
seed.remove();
|
|
170
|
+
});
|
|
139
171
|
});
|
|
140
172
|
|
|
141
173
|
describe("useTheme preset/custom shadowing", () => {
|
package/src/theme/useTheme.ts
CHANGED
|
@@ -149,7 +149,7 @@ const THEME_TRANSITION_DURATION = 300;
|
|
|
149
149
|
let transitionTimer: CronHandle | null = null;
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Theme vars live in ONE managed stylesheet block (
|
|
152
|
+
* Theme vars live in ONE managed stylesheet block (`html:root` overrides)
|
|
153
153
|
* instead of a hundred inline declarations on `<html style="...">`.
|
|
154
154
|
* - Inline style attributes bloat the DOM, show up as giant devtools
|
|
155
155
|
* noise on the root element, defeat caching of the token set and make
|
|
@@ -162,8 +162,20 @@ let transitionTimer: CronHandle | null = null;
|
|
|
162
162
|
* so a token whose value already resolves identically needs no
|
|
163
163
|
* override at all — a default-ish theme injects a handful of vars
|
|
164
164
|
* instead of ~80.
|
|
165
|
+
*
|
|
166
|
+
* The selector is `html:root`, not plain `:root`, on purpose: the static
|
|
167
|
+
* seed is `:root`-level, and lazy route chunks re-emit it (every component
|
|
168
|
+
* sheet `@use`s tokens.scss) into `<link>`s appended to `<head>` AFTER
|
|
169
|
+
* this block — a same-specificity `:root` there would silently beat the
|
|
170
|
+
* runtime palette and un-theme every late-loaded route (2026-09-18,
|
|
171
|
+
* dev.cw login: the HkOtpInput chunk flattened the brand palette back to
|
|
172
|
+
* the static seed). `html:root` (0,1,1) outranks every `:root`-level
|
|
173
|
+
* declaration (0,1,0) regardless of document order, while staying below
|
|
174
|
+
* inline styles and equal to `html[data-theme]` decor rules (order
|
|
175
|
+
* decides between those two, so a later decor can still override).
|
|
165
176
|
*/
|
|
166
177
|
const THEME_VARS_STYLE_ATTR = "data-hikari-theme-vars";
|
|
178
|
+
const THEME_VARS_SELECTOR = "html:root";
|
|
167
179
|
|
|
168
180
|
function normalizeVarValue(value: string): string {
|
|
169
181
|
return value.replace(/\s+/g, " ").trim();
|
|
@@ -214,7 +226,7 @@ function injectThemeVars(el: HTMLElement, vars: Record<string, string>): void {
|
|
|
214
226
|
|
|
215
227
|
const deltas = pickThemeVarDeltas(vars, baseline);
|
|
216
228
|
styleEl.textContent = Object.keys(deltas).length > 0
|
|
217
|
-
?
|
|
229
|
+
? `${THEME_VARS_SELECTOR}{${Object.entries(deltas).map(([key, value]) => `${key}:${value}`).join(";")}}`
|
|
218
230
|
: "";
|
|
219
231
|
}
|
|
220
232
|
|