@celestia-island/hikari 0.55.169 → 0.55.170

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.169",
3
+ "version": "0.55.170",
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",
@@ -255,15 +255,31 @@ describe("HkAdminHeader", () => {
255
255
  const centre = c.querySelector(".s-admin-header-center");
256
256
  expect(centre, "the centre wrapper must render when the slot is provided").not.toBeNull();
257
257
  expect(centre?.querySelector(".centre-stub")?.textContent).toBe("filter group");
258
- // Wing order: identity (left) → centre → actions (right).
258
+ // Wing order: identity (left) → centre → actions (right). The wings
259
+ // carry their chrome class PLUS the shared balanced-grid wing class,
260
+ // so match per-class instead of whole-className equality.
259
261
  const header = c.querySelector("header")!;
260
262
  const kids = [...header.children].map((el) => el.className);
261
- expect(kids.indexOf("s-admin-header-user")).toBeLessThan(kids.indexOf("s-admin-header-center"));
262
- expect(kids.indexOf("s-admin-header-center")).toBeLessThan(kids.indexOf("s-admin-header-actions"));
263
+ const wingIdx = (cls: string) => kids.findIndex((c) => c.split(/\s+/).includes(cls));
264
+ expect(wingIdx("s-admin-header-user")).toBeLessThan(wingIdx("s-admin-header-center"));
265
+ expect(wingIdx("s-admin-header-center")).toBeLessThan(wingIdx("s-admin-header-actions"));
266
+ // The header hangs the shared balanced-grid contract — the same
267
+ // geometry the chat frontend's header uses (user report 2026-10-15:
268
+ // the console strip sat left of centre on phones).
269
+ expect(header.className).toContain("s-balanced-header");
270
+ expect(kids.find((c) => c.includes("s-admin-header-user"))).toContain("s-header-wing-start");
271
+ expect(kids.find((c) => c.includes("s-admin-header-center"))).toContain("s-header-wing-center");
272
+ expect(kids.find((c) => c.includes("s-admin-header-actions"))).toContain("s-header-wing-end");
263
273
  });
264
274
 
265
275
  it("renders no centre wrapper without the slot (DOM unchanged for hosts that skip it)", () => {
266
276
  const c = mount(headerNode({ title: "Providers" }));
267
277
  expect(c.querySelector(".s-admin-header-center")).toBeNull();
278
+ // Centre-less hosts are the MAJORITY of admin pages: the actions wing
279
+ // must still carry its explicit grid-column class, or auto-placement
280
+ // drops it into the empty middle track and it drifts to the viewport
281
+ // centre (R2, 2026-10-15 — the class IS the column).
282
+ const actions = c.querySelector(".s-admin-header-actions");
283
+ expect(actions?.className).toContain("s-header-wing-end");
268
284
  });
269
285
  });
@@ -162,24 +162,36 @@ export const HkAdminHeader = defineComponent({
162
162
  }
163
163
  }
164
164
 
165
+ // The header hangs the shared three-zone geometry (grid: two equal
166
+ // side tracks around a shrinkable centre) — the same contract the
167
+ // chat frontend top bar uses, so a page header-centre group sits on
168
+ // the true viewport middle on BOTH faces at every width. (Comment
169
+ // lives OUTSIDE the class expression: the self-sufficiency audit
170
+ // reads that expression's literals, and prose apostrophes inside it
171
+ // parse as string boundaries.)
165
172
  return () => (
166
173
  <header
167
174
  class={[
168
175
  "s-glass-header",
176
+ "s-balanced-header",
169
177
  props.compact ? "s-glass-header--compact" : "",
170
178
  ]}
171
179
  >
172
- {props.showHamburger && (
173
- <HButton
174
- variant="ghost"
175
- size="sm"
176
- onClick={() => emit("hamburger")}
177
- >
178
- <Menu size={20} class="s-admin-header-hamburger-icon" />
179
- </HButton>
180
- )}
181
-
182
- <div ref={userTriggerRef} class="s-admin-header-user">
180
+ <div ref={userTriggerRef} class="s-admin-header-user s-header-wing-start">
181
+ {/* The hamburger rides INSIDE the left wing: the balanced grid
182
+ * above owns the header's columns, and a fourth in-flow child
183
+ * would fall into the left wing's track and skew every column
184
+ * after it. Inside the wing it keeps its place (leftmost, ahead
185
+ * of the avatar) at the wing's own 16px rhythm. */}
186
+ {props.showHamburger && (
187
+ <HButton
188
+ variant="ghost"
189
+ size="sm"
190
+ onClick={() => emit("hamburger")}
191
+ >
192
+ <Menu size={20} class="s-admin-header-hamburger-icon" />
193
+ </HButton>
194
+ )}
183
195
  <button
184
196
  class="s-admin-header-avatar"
185
197
  aria-label={props.avatarTriggerLabel ?? t("hikari::adminHeader.avatarTrigger", "Account menu")}
@@ -284,19 +296,19 @@ export const HkAdminHeader = defineComponent({
284
296
  </HkMenu>
285
297
 
286
298
  {/* Page-owned centre group (page filter tab strips, segmented
287
- * option pickers) — the console face of the balanced-wing
288
- * header the chat frontend's top bar already uses: the left
289
- * and right wings grow from the same flex basis so this group
290
- * stays pinned to the true viewport middle. Rendered only when
291
- * the host provides it, so headers without a centre keep the
292
- * exact DOM they always had. */}
299
+ * option pickers) — the console face of the balanced header the
300
+ * chat frontend's top bar already uses: the shared grid pins
301
+ * this column to the true viewport middle and lets the strip
302
+ * slide inside its own box when space runs short. Rendered only
303
+ * when the host provides it, so headers without a centre keep
304
+ * the exact DOM they always had. */}
293
305
  {slots.center && (
294
- <div class="s-admin-header-center">
306
+ <div class="s-admin-header-center s-header-wing-center">
295
307
  {slots.center()}
296
308
  </div>
297
309
  )}
298
310
 
299
- <div class="s-admin-header-actions">
311
+ <div class="s-admin-header-actions s-header-wing-end">
300
312
  {props.showEmergencyStop && (
301
313
  <button
302
314
  class={[
@@ -93,20 +93,59 @@ describe("admin header chrome geometry (the two user-requested fixes)", () => {
93
93
  const rule = ruleDeclarations(adminTokens, ".s-admin-header-user");
94
94
  expect(rule, ".s-admin-header-user must be declared").not.toBeNull();
95
95
  expect(rule).toContainEqual(["gap", "var(--space-16)"]);
96
- // Balanced left wing (the centre group stays viewport-centred).
97
- expect(rule).toContainEqual(["flex", "1 1 0"]);
98
96
  });
99
97
 
100
- it("keeps the actions wing balanced (no margin-left:auto push) and the centre wing shrinkable", () => {
101
- const actions = ruleDeclarations(adminTokens, ".s-admin-header-actions");
102
- expect(actions, ".s-admin-header-actions must be declared").not.toBeNull();
103
- expect(actions).toContainEqual(["flex", "1 1 0"]);
104
- expect(actions).toContainEqual(["justify-content", "flex-end"]);
98
+ it("hangs the shared balanced-header grid contract (the ONE header layout)", () => {
99
+ // User report 2026-10-15: the console's header-centre tab strip sat
100
+ // LEFT of centre on phones (drawer mode hides the title, so the left
101
+ // wing is just the avatar while three icon buttons sit right) while
102
+ // the frontend's strip read centred — two per-band flex dialects had
103
+ // drifted from the same idea. Both faces now hang the ONE grid
104
+ // contract: two EQUAL 1fr side tracks (true centring regardless of
105
+ // wing content) with min-content floors (wings can never be squeezed
106
+ // out of existence) around a shrinkable centre column (the strip
107
+ // slides inside its own box, edge fades + swipe).
108
+ const balanced = ruleDeclarations(adminTokens, ".s-balanced-header");
109
+ expect(balanced, ".s-balanced-header must be declared").not.toBeNull();
110
+ expect(balanced).toContainEqual([
111
+ "grid-template-columns",
112
+ "minmax(min-content, 1fr) minmax(0, auto) minmax(min-content, 1fr)",
113
+ ]);
114
+ const start = ruleDeclarations(adminTokens, ".s-header-wing-start");
115
+ expect(start, ".s-header-wing-start must be declared").not.toBeNull();
116
+ // Each wing names its column EXPLICITLY: with auto-placement a
117
+ // centre-less host (the vast majority of admin pages — most never
118
+ // register a centre group) renders only two in-flow children and the
119
+ // actions wing would fall into the MIDDLE track, drifting to the
120
+ // viewport centre instead of hugging the right edge (R2, 2026-10-15).
121
+ expect(start).toContainEqual(["grid-column", "1"]);
122
+ expect(start).toContainEqual(["justify-self", "start"]);
123
+ expect(start).toContainEqual(["overflow", "hidden"]);
124
+ const centre = ruleDeclarations(adminTokens, ".s-header-wing-center");
125
+ expect(centre, ".s-header-wing-center must be declared").not.toBeNull();
126
+ // Default stretch, NOT justify-self: center: a centered item sizes by
127
+ // fit-content, whose min-content floor (the full strip width) punches
128
+ // the item past a shrunken middle track — the strip would paint over
129
+ // the wings instead of scrolling inside its own box.
130
+ expect(centre).toContainEqual(["grid-column", "2"]);
131
+ expect(centre).toContainEqual(["min-width", "0"]);
132
+ expect(centre).not.toContainEqual(["justify-self", "center"]);
133
+ const end = ruleDeclarations(adminTokens, ".s-header-wing-end");
134
+ expect(end, ".s-header-wing-end must be declared").not.toBeNull();
135
+ expect(end).toContainEqual(["grid-column", "3"]);
136
+ expect(end).toContainEqual(["justify-self", "end"]);
137
+ expect(end).toContainEqual(["overflow", "hidden"]);
138
+ });
139
+
140
+ it("keeps the wings free of their own flex-geometry dialect (the grid owns it)", () => {
141
+ // The flex era's per-wing declarations must stay gone: any return of
142
+ // `flex` on a wing re-introduces a second geometry that drifts from
143
+ // the shared grid (the 2026-10-15 skew).
144
+ expect(ruleDeclarations(adminTokens, ".s-admin-header-user")).not.toContainEqual(["flex", "1 1 0"]);
145
+ expect(ruleDeclarations(adminTokens, ".s-admin-header-actions")).not.toContainEqual(["flex", "1 1 0"]);
146
+ expect(ruleDeclarations(adminTokens, ".s-admin-header-center")).not.toContainEqual(["flex", "0 1 auto"]);
105
147
  // The old push would fight the centre wing for the middle.
106
- expect(actions).not.toContainEqual(["margin-left", "auto"]);
107
- const centre = ruleDeclarations(adminTokens, ".s-admin-header-center");
108
- expect(centre, ".s-admin-header-center must be declared").not.toBeNull();
109
- expect(centre).toContainEqual(["flex", "0 1 auto"]);
148
+ expect(ruleDeclarations(adminTokens, ".s-admin-header-actions")).not.toContainEqual(["margin-left", "auto"]);
110
149
  });
111
150
 
112
151
  it("keeps the 28px icon-button step at the theme toggle's own box", () => {
@@ -120,25 +159,20 @@ describe("admin header chrome geometry (the two user-requested fixes)", () => {
120
159
  expect(iconButton).toMatch(/PropType<16 \| 24 \| 28 \| 32 \| 36 \| 40>/);
121
160
  });
122
161
 
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.
162
+ it("keeps the phone band free of ad-hoc wing overrides (one geometry at every width)", () => {
163
+ // The 2026-10-12 band pinned the wings and handed the leftover to
164
+ // the centre; it fixed the avatar overlap but skewed the strip off
165
+ // the viewport middle (user report 2026-10-15). The shared grid's
166
+ // min-content floors + shrinkable centre column handle phones
167
+ // intrinsically, so no per-band flex override may come back.
130
168
  const userRules = allRuleDeclarations(adminTokens, ".s-admin-header-user");
131
169
  const centreRules = allRuleDeclarations(adminTokens, ".s-admin-header-center");
132
170
  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"]);
171
+ for (const [name, rules] of [["user", userRules], ["centre", centreRules], ["actions", actionsRules]] as const) {
172
+ expect(rules.filter((r) => r.nested), `${name}: no band-nested rules may remain`).toHaveLength(0);
173
+ for (const r of rules) {
174
+ expect(r.decls.map((d) => d[0]), `${name}: no flex geometry may remain`).not.toContain("flex");
175
+ }
176
+ }
143
177
  });
144
178
  });
@@ -413,6 +413,74 @@
413
413
  padding: 0 var(--space-16);
414
414
  }
415
415
 
416
+ /* ── Balanced header columns (the ONE three-zone header layout) ─────────
417
+ * The shared geometry behind every top bar that owns a header-centre
418
+ * group: the frontend chat header (chest ChatLayout's .s-header-bar-*)
419
+ * and the console header (HkAdminHeader's .s-admin-header-*) both hang
420
+ * these classes next to their own chrome classes, so the two faces can
421
+ * never drift apart again (user report 2026-10-15: the console's
422
+ * header-centre tab strip sat LEFT of centre on phones while the
423
+ * frontend's strip read centred — two per-band flex dialects had grown
424
+ * from the same idea).
425
+ *
426
+ * Grid, not flex: two EQUAL 1fr side tracks pin the middle column to the
427
+ * true viewport middle no matter how lopsided the wings' CONTENT is
428
+ * (avatar-only left vs three icon buttons right — exactly the console's
429
+ * phone case, where the drawer mode hides the title and the left wing is
430
+ * just the 28px trigger). Flex wings grow from flex-basis 0, so equal
431
+ * growth centred the group only while spare space lasted; each band then
432
+ * grew its own ad-hoc override and each override skewed in SOME band.
433
+ * `minmax(min-content, 1fr)` gives each wing a hard content floor, so an
434
+ * overflowing centre column (a wide tab strip) can never squeeze the
435
+ * avatar/action wings out of existence — the 2026-10-12 phone incident
436
+ * class — and the centre column is `minmax(0, auto)`, so IT soaks the
437
+ * squeeze instead: the strip narrows while STAYING centred, and its
438
+ * internal HkTabs scroller takes over (edge fades + swipe, the frontend
439
+ * over-wide-strip behavior) without ever covering a wing. */
440
+ .s-balanced-header {
441
+ display: grid;
442
+ grid-template-columns: minmax(min-content, 1fr) minmax(0, auto) minmax(min-content, 1fr);
443
+ align-items: center;
444
+ }
445
+
446
+ /* Each wing names its column EXPLICITLY: the header's in-flow children are
447
+ * exactly these three zones (every other child teleports out), but a
448
+ * centre-less host renders only two of them — with auto-placement the
449
+ * actions wing would fall into the MIDDLE track and drift to the viewport
450
+ * centre (R2 finding, 2026-10-15), because the trailing 1fr track would
451
+ * sit empty. Naming the columns makes the contract positional by class,
452
+ * not by child order or count: a centre-less header lays out as
453
+ * [user | empty | actions] exactly like the old flex bar did. */
454
+ .s-header-wing-start {
455
+ grid-column: 1;
456
+ justify-self: start;
457
+ min-width: 0;
458
+ /* A wing whose content floors exceed its track must not PAINT past the
459
+ * wing box underneath the centre group (popup surfaces teleport to
460
+ * body, so clipping here never cuts a menu). */
461
+ overflow: hidden;
462
+ }
463
+
464
+ .s-header-wing-center {
465
+ /* Default stretch, deliberately: the strip must inherit the COLUMN's
466
+ * shrunken width when the middle track soaks a squeeze, so the HTabs
467
+ * scroller inside it takes over (edge fades + swipe). justify-self:
468
+ * center here would size the item by fit-content, whose min-content
469
+ * floor (= the full strip width) punches the item past the shrunken
470
+ * track and the strip paints over the wings instead of scrolling.
471
+ * Centring itself is the tracks' job (equal 1fr sides), not the
472
+ * item's. */
473
+ grid-column: 2;
474
+ min-width: 0;
475
+ }
476
+
477
+ .s-header-wing-end {
478
+ grid-column: 3;
479
+ justify-self: end;
480
+ min-width: 0;
481
+ overflow: hidden;
482
+ }
483
+
416
484
  /* ── Admin header chrome (HkAdminHeader) ────────────────────────────────
417
485
  * The header's own layout. Every declaration below replaces a utility
418
486
  * class the markup used to depend on (`w-7 h-7`, `rounded-full`,
@@ -420,7 +488,10 @@
420
488
  * atomic layer rendered the header without them, and a host with one
421
489
  * (chest/UnoCSS) owned values the design system could not fix. Values are
422
490
  * the verified computed ones (audit 2026-09-15 against chest's generated
423
- * CSS), so no host's rendering changes. */
491
+ * CSS), so no host's rendering changes.
492
+ * The three-zone GEOMETRY itself lives in the shared .s-balanced-header
493
+ * contract above — the classes below keep only their wing-INTERNAL
494
+ * layout (row direction, gaps, alignment) and appearance. */
424
495
 
425
496
  .s-admin-header-user {
426
497
  display: flex;
@@ -433,50 +504,15 @@
433
504
  * The wing gap here carries the whole distance so the title text
434
505
  * lands on the same rhythm as the panel's button content. */
435
506
  gap: var(--space-16);
436
- /* Balanced left wing (same grammar as ChatLayout's .s-header-bar-left):
437
- * grows from the same flex basis as the actions wing so a centre
438
- * group, when a page renders one, stays pinned to the true middle. */
439
- flex: 1 1 0;
440
- min-width: 0;
441
507
  }
442
508
 
443
509
  /* The page-owned centre group (filter tab strips): keeps its natural
444
- * width and hands overflow to the HTabs scroller inside it. */
510
+ * width and hands overflow to the HTabs scroller inside it. Column
511
+ * placement/shrinkage is the shared grid contract's job. */
445
512
  .s-admin-header-center {
446
- flex: 0 1 auto;
447
513
  display: flex;
448
514
  align-items: center;
449
515
  justify-content: center;
450
- min-width: 0;
451
- }
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
516
  }
481
517
 
482
518
  .s-admin-header-avatar {
@@ -527,13 +563,10 @@
527
563
  display: flex;
528
564
  align-items: center;
529
565
  gap: var(--space-6);
530
- /* Balanced right wing: with the user wing at flex 1 1 0 the pair
531
- * grows symmetrically, so the centre group stays centred and the
532
- * actions still hug the right edge (justify-end replaces the old
533
- * margin-left:auto push, which would fight the centre wing). The
534
- * automatic min-content floor keeps the icon buttons from being
535
- * squeezed below their own width on narrow viewports. */
536
- flex: 1 1 0;
566
+ /* The actions hug the right edge of their grid column (the old
567
+ * margin-left:auto push fought the centre wing for the middle; the
568
+ * flex-balanced `1 1 0` era before that skewed whenever the wings'
569
+ * contents disagreed — the shared grid contract ends both). */
537
570
  justify-content: flex-end;
538
571
  }
539
572