@celestia-island/hikari 0.55.26 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.26",
3
+ "version": "0.55.27",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -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 a :root block, not an inline attribute.
123
- expect(styleEl!.textContent).toMatch(/^:root\{/);
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(/^:root\{/);
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", () => {
@@ -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 (`:root` overrides)
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
- ? `:root{${Object.entries(deltas).map(([key, value]) => `${key}:${value}`).join(";")}}`
229
+ ? `${THEME_VARS_SELECTOR}{${Object.entries(deltas).map(([key, value]) => `${key}:${value}`).join(";")}}`
218
230
  : "";
219
231
  }
220
232