@celestia-island/hikari 0.55.204 → 0.55.205

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.204",
3
+ "version": "0.55.205",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -20,6 +20,14 @@ export const HkAdminHeader = defineComponent({
20
20
  title: { type: String, default: "" },
21
21
  showHamburger: { type: Boolean, default: false },
22
22
  compact: { type: Boolean, default: false },
23
+ /** Desktop shell face whose header IS the window title bar (Tauri
24
+ * decorations:false): pass true iff the header renders the
25
+ * HkWindowControls cluster, so the right inset follows the
26
+ * window-controls chrome contract (.s-glass-header--chrome /
27
+ * --hk-window-controls-edge-inset) instead of the content inset —
28
+ * a standard-mode cluster glued to the frame corner is the flush
29
+ * variant's look worn by the wrong mode (field report 2026-10-25). */
30
+ chrome: { type: Boolean, default: false },
23
31
  actions: { type: Array as PropType<VNode[]>, default: () => [] },
24
32
  /** Signed-in identity. Drives the avatar fallback letter, the
25
33
  * dropdown identity block and the pending-state branch — it is
@@ -192,6 +200,7 @@ export const HkAdminHeader = defineComponent({
192
200
  "s-glass-header",
193
201
  "s-balanced-header",
194
202
  props.compact ? "s-glass-header--compact" : "",
203
+ props.chrome ? "s-glass-header--chrome" : "",
195
204
  ]}
196
205
  >
197
206
  <div ref={userTriggerRef} class="s-admin-header-user s-header-wing-start">
@@ -3,7 +3,12 @@
3
3
  // standard: borderless ghost buttons at the header chrome's own size
4
4
  // step (2rem boxes, 16px glyphs, 0.375rem gaps) — no borders, no
5
5
  // dividers, no container chrome; the close hover is a small rounded
6
- // red wash. Reads as one family with gear/search icon buttons.
6
+ // red wash. Reads as one family with gear/search icon buttons. The
7
+ // HOST keeps the canonical right edge inset
8
+ // (--hk-window-controls-edge-inset, declared in styles/admin-tokens)
9
+ // — a standard cluster never rides the frame; gluing it to the corner
10
+ // is the flush mode's look worn by the wrong mode (field report
11
+ // 2026-10-25: the chest login strip had no right padding at all).
7
12
  //
8
13
  // flush: edge-riding squares for thin captions — the host zeroes its
9
14
  // own right padding (the s-header-bar--chrome contract pattern); the
@@ -15,6 +15,17 @@ import "./HkWindowControls.scss";
15
15
  * with comfortable gaps. The whole top-right reads as one family of
16
16
  * identical buttons; the close hover is a small rounded red wash.
17
17
  * THE mode for content-height headers (3rem-class bars).
18
+ * HOST CONTRACT: this mode never rides the window edge (that is the
19
+ * flush variant's job) — the host keeps the canonical inset
20
+ * `var(--hk-window-controls-edge-inset)` (≤1023px:
21
+ * `--hk-window-controls-edge-inset-compact`) between the cluster
22
+ * and the frame — token values live once in styles/admin-tokens,
23
+ * not restated here. A standard cluster glued to the frame corner
24
+ * is the wrong mode's look (field report 2026-10-25: the chest
25
+ * login strip rendered with no end padding at all);
26
+ * `.s-glass-header--chrome` is the in-library implementation of the
27
+ * contract for admin headers, and the chat face sources its
28
+ * `--chrome` padding from the same tokens.
18
29
  *
19
30
  * - `flush` — edge-riding squares that fill the host's height (the
20
31
  * host must zero its own right padding so the close button rides
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ /**
7
+ * Static pins for the window-controls chrome-edge contract (field report
8
+ * 2026-10-25: the chest login strip rendered the standard HkWindowControls
9
+ * cluster glued to the frame corner — zero right padding — while the chat
10
+ * face had ruled the inset to 12px/8px and the console face kept a full
11
+ * 24px of dead air; three faces, three ad-hoc computations of the same
12
+ * number). happy-dom has no layout engine, so — adminHeaderChrome-style —
13
+ * these read the declarations themselves; reverting any pinned value keeps
14
+ * the rest of the suite green, so each pin below is the only red-if-broken
15
+ * evidence for its contract. A deliberate rewrite is expected to update
16
+ * them.
17
+ *
18
+ * Pinned here:
19
+ * 1. the canonical tokens exist at :root (admin-tokens.scss top level —
20
+ * a nested declaration would not reach host headers);
21
+ * 2. .s-glass-header--chrome sources BOTH bands from the tokens (the
22
+ * in-library implementation of the host contract);
23
+ * 3. HkAdminHeader actually hangs the class from the `chrome` prop
24
+ * (the prop without the class, or vice versa, ships the 24px inset);
25
+ * 4. the standard-mode HOST CONTRACT is documented on HkWindowControls
26
+ * (the doc is the contract's prose home — keep it in sync).
27
+ */
28
+
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+
31
+ const adminTokens = readFileSync(join(here, "..", "styles", "admin-tokens.scss"), "utf8");
32
+ const adminHeader = readFileSync(join(here, "HkAdminHeader.tsx"), "utf8");
33
+ const windowControls = readFileSync(join(here, "HkWindowControls.tsx"), "utf8");
34
+
35
+ /** Brace depth of `code` at character index `at` (0 = top level). */
36
+ function braceDepthAt(code: string, at: number): number {
37
+ let depth = 0;
38
+ for (let i = 0; i < at; i += 1) {
39
+ if (code[i] === "{") depth += 1;
40
+ else if (code[i] === "}") depth -= 1;
41
+ }
42
+ return depth;
43
+ }
44
+
45
+ /** Declarations of every rule whose prelude IS `selector` (whole-prelude
46
+ * match, comments stripped), with whether it sits inside an at-block. */
47
+ function allRuleDeclarations(
48
+ source: string,
49
+ selector: string,
50
+ ): Array<{ nested: boolean; decls: Array<[string, string]> }> {
51
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "");
52
+ const out: Array<{ nested: boolean; decls: Array<[string, string]> }> = [];
53
+ for (let at = code.indexOf(selector); at >= 0; at = code.indexOf(selector, at + 1)) {
54
+ const open = code.indexOf("{", at + selector.length);
55
+ if (open < 0) continue;
56
+ if (code.slice(at + selector.length, open).trim() !== "") continue;
57
+ let boundary = at - 1;
58
+ while (boundary >= 0 && !"}{;".includes(code[boundary])) boundary -= 1;
59
+ if (code.slice(boundary + 1, at).replace(/\s+/g, " ").trim() !== "") continue;
60
+ const nested = braceDepthAt(code, at) > 0;
61
+ const body = code.slice(open + 1, code.indexOf("}", open));
62
+ const decls: Array<[string, string]> = [];
63
+ for (const part of body.split(";")) {
64
+ const m = /^\s*([-a-zA-Z]+)\s*:\s*([\s\S]+?)\s*$/.exec(part);
65
+ if (m) decls.push([m[1], m[2].replace(/\s+/g, " ")]);
66
+ }
67
+ out.push({ nested, decls });
68
+ }
69
+ return out;
70
+ }
71
+
72
+ describe("window-controls chrome-edge contract (the three faces had drifted)", () => {
73
+ it("declares the canonical edge-inset tokens at :root top level", () => {
74
+ // :root is a top-level rule; a token declared inside an at-block (or
75
+ // renamed) silently falls every consumer back to its var() fallback —
76
+ // the token must exist AND sit where it cascades.
77
+ const rootRules = allRuleDeclarations(adminTokens, ":root").filter((r) => !r.nested);
78
+ expect(rootRules.length, ":root must be declared at top level").toBeGreaterThan(0);
79
+ const joined = rootRules.flatMap((r) => r.decls);
80
+ expect(joined).toContainEqual(["--hk-window-controls-edge-inset", "0.75rem"]);
81
+ expect(joined).toContainEqual(["--hk-window-controls-edge-inset-compact", "0.5rem"]);
82
+ });
83
+
84
+ it("sources the chrome header's desktop inset from the token (logical property)", () => {
85
+ // padding-INLINE-END, not padding-right: in a dir="rtl" host the
86
+ // caption row flips to the visual left and the inset must flip with
87
+ // it (same reasoning as the RTL-mirrored titlebar close button).
88
+ const rules = allRuleDeclarations(adminTokens, ".s-glass-header.s-glass-header--chrome").filter(
89
+ (r) => !r.nested,
90
+ );
91
+ expect(rules.length, ".s-glass-header--chrome must be declared at top level").toBe(1);
92
+ expect(rules[0].decls).toContainEqual([
93
+ "padding-inline-end",
94
+ "var(--hk-window-controls-edge-inset, 0.75rem)",
95
+ ]);
96
+ });
97
+
98
+ it("sources the chrome header's ≤1023px inset from the compact token", () => {
99
+ const rules = allRuleDeclarations(adminTokens, ".s-glass-header.s-glass-header--chrome").filter(
100
+ (r) => r.nested,
101
+ );
102
+ expect(rules.length, "the compact band must override the chrome inset").toBe(1);
103
+ expect(rules[0].decls).toContainEqual([
104
+ "padding-inline-end",
105
+ "var(--hk-window-controls-edge-inset-compact, 0.5rem)",
106
+ ]);
107
+ });
108
+
109
+ it("hangs the chrome class from the HkAdminHeader chrome prop", () => {
110
+ // The prop without the class (or the class without a producer) leaves
111
+ // the console face on the base 24px inset — both halves are the pin.
112
+ expect(adminHeader).toMatch(/chrome:\s*\{\s*type:\s*Boolean,\s*default:\s*false\s*\}/);
113
+ expect(adminHeader).toMatch(
114
+ /props\.chrome\s*\?\s*"s-glass-header--chrome"\s*:\s*""/,
115
+ );
116
+ });
117
+
118
+ it("documents the standard-mode host contract on HkWindowControls", () => {
119
+ // The doc comment is the contract's prose home: the token name and the
120
+ // never-flush rule must stay stated where hosts read the component.
121
+ expect(windowControls).toContain("HOST CONTRACT");
122
+ expect(windowControls).toContain("--hk-window-controls-edge-inset");
123
+ expect(windowControls).toContain("never rides the window edge");
124
+ });
125
+ });
@@ -9,6 +9,19 @@
9
9
  /* ── Layout tokens ─ */
10
10
  --s-footer-height: 3rem;
11
11
  --s-header-height: 3rem;
12
+
13
+ /* ── Window-control chrome insets ─
14
+ * The right padding a host title-bar header keeps between the standard
15
+ * HkWindowControls cluster and the window edge (canonical home since
16
+ * the chest login strip shipped the cluster glued to the frame corner —
17
+ * field report 2026-10-25). The standard cluster NEVER rides the edge
18
+ * (that is the flush variant's job), so every chrome face sources its
19
+ * padding from here instead of restating half-of-the-header-inset per
20
+ * face — the faces had drifted three ways (chat 12px/8px, console a
21
+ * full 24px of dead air, auth 0px). Values are the 2026-10-23 ruling's
22
+ * halvings of the base/compact header insets, promoted to tokens. */
23
+ --hk-window-controls-edge-inset: 0.75rem; /* desktop: half of the 24px base header inset */
24
+ --hk-window-controls-edge-inset-compact: 0.5rem; /* ≤1023px: half of the 16px compact header inset */
12
25
  /* Layout/type/motion scale tokens (--space-*, --text-*, --blur-*, --z-*,
13
26
  * --duration-*, --ease-*) used to be mirrored here ("keep both in sync")
14
27
  * — they now live once in styles/theme/scale.scss, which every entry
@@ -437,6 +450,27 @@
437
450
  padding: 0 var(--space-16);
438
451
  }
439
452
 
453
+ /* Chrome variant (HkAdminHeader `chrome` prop — the desktop face whose
454
+ * header IS the window title bar): the inline-end inset follows the
455
+ * window-controls contract token, not the content inset — the default
456
+ * 24px reads as dead air next to the standard HkWindowControls cluster
457
+ * (the same 2026-10-23 user direction that halved the chat face's gap).
458
+ * LOGICAL property, like the RTL-mirrored titlebar close button (the
459
+ * [dir="rtl"] .hk-titlebar-btn-close rule in components/HkTitleBar.scss):
460
+ * in a dir="rtl" host the caption row flips to the visual left and the
461
+ * inset must flip with it, not stay on the physical right. (0,2,0)
462
+ * outranks the base rule and the compact band alike, so the chrome
463
+ * inset wins regardless of load order. */
464
+ .s-glass-header.s-glass-header--chrome {
465
+ padding-inline-end: var(--hk-window-controls-edge-inset, 0.75rem);
466
+ }
467
+
468
+ @media (max-width: 1023px) {
469
+ .s-glass-header.s-glass-header--chrome {
470
+ padding-inline-end: var(--hk-window-controls-edge-inset-compact, 0.5rem);
471
+ }
472
+ }
473
+
440
474
  /* ── Balanced header columns (the ONE three-zone header layout) ─────────
441
475
  * The shared geometry behind every top bar that owns a header-centre
442
476
  * group: the frontend chat header (chest ChatLayout's .s-header-bar-*)