@celestia-island/hikari 0.55.154 → 0.55.155

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.154",
3
+ "version": "0.55.155",
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",
@@ -27,6 +27,9 @@ function ruleDeclarations(source: string, selector: string): Array<[string, stri
27
27
  let boundary = at - 1;
28
28
  while (boundary >= 0 && !"}{;".includes(code[boundary])) boundary -= 1;
29
29
  if (code.slice(boundary + 1, at).replace(/\s+/g, " ").trim() !== "") continue;
30
+ // Top-level rules only: a nested occurrence (the ≤767px phone band)
31
+ // must not satisfy the desktop pins.
32
+ if (braceDepthAt(code, at) !== 0) continue;
30
33
  const body = code.slice(open + 1, code.indexOf("}", open));
31
34
  const out: Array<[string, string]> = [];
32
35
  for (const part of body.split(";")) {
@@ -38,10 +41,50 @@ function ruleDeclarations(source: string, selector: string): Array<[string, stri
38
41
  return null;
39
42
  }
40
43
 
44
+
45
+ /** Brace depth of `code` at character index `at` — 0 = top level, ≥1 =
46
+ * nested inside an at-block (e.g. the ≤767px phone band's @media). */
47
+ function braceDepthAt(code: string, at: number): number {
48
+ let depth = 0;
49
+ for (let i = 0; i < at; i += 1) {
50
+ if (code[i] === "{") depth += 1;
51
+ else if (code[i] === "}") depth -= 1;
52
+ }
53
+ return depth;
54
+ }
55
+
41
56
  const adminTokens = readFileSync(join(here, "..", "styles", "admin-tokens.scss"), "utf8");
42
57
  const iconButtonVars = readFileSync(join(here, "HkIconButtonVars.scss"), "utf8");
43
58
  const iconButton = readFileSync(join(here, "HkIconButton.tsx"), "utf8");
44
59
 
60
+ /** Every rule whose prelude IS `selector` (whole-prelude match, comments
61
+ * stripped) — used for the phone band, where the same prelude appears
62
+ * both at top level (desktop) and inside the ≤767px media block. */
63
+ function allRuleDeclarations(
64
+ source: string,
65
+ selector: string,
66
+ ): Array<{ nested: boolean; decls: Array<[string, string]> }> {
67
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "");
68
+ const out: Array<{ nested: boolean; decls: Array<[string, string]> }> = [];
69
+ for (let at = code.indexOf(selector); at >= 0; at = code.indexOf(selector, at + 1)) {
70
+ const open = code.indexOf("{", at + selector.length);
71
+ if (open < 0) continue;
72
+ if (code.slice(at + selector.length, open).trim() !== "") continue;
73
+ let boundary = at - 1;
74
+ while (boundary >= 0 && !"}{;".includes(code[boundary])) boundary -= 1;
75
+ if (code.slice(boundary + 1, at).replace(/\s+/g, " ").trim() !== "") continue;
76
+ const nested = braceDepthAt(code, at) > 0;
77
+ const body = code.slice(open + 1, code.indexOf("}", open));
78
+ const decls: Array<[string, string]> = [];
79
+ for (const part of body.split(";")) {
80
+ const m = /^\s*([-a-zA-Z]+)\s*:\s*([\s\S]+?)\s*$/.exec(part);
81
+ if (m) decls.push([m[1], m[2].replace(/\s+/g, " ")]);
82
+ }
83
+ out.push({ nested, decls });
84
+ }
85
+ return out;
86
+ }
87
+
45
88
  describe("admin header chrome geometry (the two user-requested fixes)", () => {
46
89
  it("keeps the console title at the panel's visual distance from the avatar (16px wing gap)", () => {
47
90
  // The frontend's workspace button sits a flex gap PLUS its own 8px
@@ -76,4 +119,26 @@ describe("admin header chrome geometry (the two user-requested fixes)", () => {
76
119
  // The component accepts the step.
77
120
  expect(iconButton).toMatch(/PropType<16 \| 24 \| 28 \| 32 \| 36 \| 40>/);
78
121
  });
122
+
123
+ it("pins the phone band: wings keep their floors, the centre strip owns the overflow", () => {
124
+ // User report 2026-10-12: on phones the balanced wings (flex 1 1 0 +
125
+ // min-width 0) were squeezed BELOW the avatar's 28px floor by a wide
126
+ // centre tab strip, and the strip painted straight over the avatar.
127
+ // The ≤767px band must pin both wings and hand the leftover to the
128
+ // centre (mirroring ChatLayout's own phone band) so the strip slides
129
+ // inside its own box instead of over the avatar.
130
+ const userRules = allRuleDeclarations(adminTokens, ".s-admin-header-user");
131
+ const centreRules = allRuleDeclarations(adminTokens, ".s-admin-header-center");
132
+ const actionsRules = allRuleDeclarations(adminTokens, ".s-admin-header-actions");
133
+ // Exactly one NESTED occurrence (the phone band) per prelude.
134
+ const phone = (rules: typeof userRules) => rules.filter((r) => r.nested).map((r) => r.decls);
135
+ expect(phone(userRules)).toHaveLength(1);
136
+ expect(phone(centreRules)).toHaveLength(1);
137
+ expect(phone(actionsRules)).toHaveLength(1);
138
+ expect(phone(userRules)[0]).toContainEqual(["flex", "0 0 auto"]);
139
+ expect(phone(actionsRules)[0]).toContainEqual(["flex", "0 0 auto"]);
140
+ expect(phone(centreRules)[0]).toContainEqual(["flex", "1 1 auto"]);
141
+ expect(phone(centreRules)[0]).toContainEqual(["min-width", "0"]);
142
+ expect(phone(centreRules)[0]).toContainEqual(["overflow", "hidden"]);
143
+ });
79
144
  });
@@ -450,6 +450,35 @@
450
450
  min-width: 0;
451
451
  }
452
452
 
453
+
454
+ /* Phone band (≤767px) — mirrors ChatLayout's own ≤767px header band
455
+ * (the console face of the same balanced-wing header). On phones the
456
+ * desktop balanced wings (`flex: 1 1 0` + `min-width: 0`) let a wide
457
+ * centre tab strip squeeze BOTH side wings below their content floors:
458
+ * the avatar wing shrank under its 28px trigger and the strip painted
459
+ * straight over the avatar (user report 2026-10-12, L2 firewall tabs).
460
+ * The frontend's answer — pin both wings, hand the leftover to the
461
+ * centre and let its HTabs scroller own the overflow — is reproduced
462
+ * here in substance: the strip keeps every pixel the avatar and the action
463
+ * buttons don't need, and slides INSIDE its own box (edge fades + scroll
464
+ * limits intact) without ever covering the avatar.
465
+ * Desktop (>767px) keeps the balanced-wing centring untouched. */
466
+ @media (max-width: 767px) {
467
+ .s-admin-header-user {
468
+ flex: 0 0 auto;
469
+ }
470
+
471
+ .s-admin-header-actions {
472
+ flex: 0 0 auto;
473
+ }
474
+
475
+ .s-admin-header-center {
476
+ flex: 1 1 auto;
477
+ min-width: 0;
478
+ overflow: hidden;
479
+ }
480
+ }
481
+
453
482
  .s-admin-header-avatar {
454
483
  position: relative;
455
484
  width: var(--space-28);