@celestia-island/hikari 0.55.86 → 0.55.87

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.86",
3
+ "version": "0.55.87",
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",
@@ -247,4 +247,23 @@ describe("HkAdminHeader", () => {
247
247
  const c = mount(headerNode({ username: "alice" }));
248
248
  expect(c.textContent.toLowerCase()).not.toContain("emergency");
249
249
  });
250
+
251
+ it("renders the page centre group between the identity and the actions when provided", () => {
252
+ const c = mount(headerNode({ title: "My tasks" }, {
253
+ center: () => h("nav", { class: "centre-stub" }, "filter group"),
254
+ }));
255
+ const centre = c.querySelector(".s-admin-header-center");
256
+ expect(centre, "the centre wrapper must render when the slot is provided").not.toBeNull();
257
+ expect(centre?.querySelector(".centre-stub")?.textContent).toBe("filter group");
258
+ // Wing order: identity (left) → centre → actions (right).
259
+ const header = c.querySelector("header")!;
260
+ 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
+ });
264
+
265
+ it("renders no centre wrapper without the slot (DOM unchanged for hosts that skip it)", () => {
266
+ const c = mount(headerNode({ title: "Providers" }));
267
+ expect(c.querySelector(".s-admin-header-center")).toBeNull();
268
+ });
250
269
  });
@@ -283,6 +283,19 @@ export const HkAdminHeader = defineComponent({
283
283
  }}
284
284
  </HkMenu>
285
285
 
286
+ {/* 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. */}
293
+ {slots.center && (
294
+ <div class="s-admin-header-center">
295
+ {slots.center()}
296
+ </div>
297
+ )}
298
+
286
299
  <div class="s-admin-header-actions">
287
300
  {props.showEmergencyStop && (
288
301
  <button
@@ -9,7 +9,12 @@ export default defineComponent({
9
9
  props: {
10
10
  icon: { type: String, default: "" },
11
11
  variant: { type: String as PropType<"ghost" | "primary" | "secondary" | "danger" | "success">, default: "ghost" },
12
- size: { type: Number as PropType<16 | 24 | 32 | 36 | 40>, default: 32 },
12
+ // 28 is the header chrome step: HkThemeToggle's trigger buttons are
13
+ // 28px boxes with 16px glyphs, so a header action that must read as
14
+ // "the same kind of button" (global search, the console pages'
15
+ // refresh) composes at 28 — 24 read visibly smaller beside the toggle
16
+ // (user report 2026-09-28: 前台全局搜索与主题切换按钮不同尺寸).
17
+ size: { type: Number as PropType<16 | 24 | 28 | 32 | 36 | 40>, default: 32 },
13
18
  disabled: { type: Boolean, default: false },
14
19
  },
15
20
  emits: {
@@ -72,6 +72,11 @@
72
72
  --hi-icon-button-icon-size: var(--hi-icon-size-xs);
73
73
  }
74
74
 
75
+ .hk-icon-button-28 {
76
+ --hi-icon-button-size: 28px;
77
+ --hi-icon-button-icon-size: var(--hi-icon-size-xs);
78
+ }
79
+
75
80
  .hk-icon-button-32 {
76
81
  --hi-icon-button-size: 32px;
77
82
  --hi-icon-button-icon-size: var(--hi-icon-size-sm);
@@ -0,0 +1,79 @@
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 admin header's chrome geometry (2026-09-28 user
8
+ * report, R1 findings b/c: reverting either the avatar→title gap or the
9
+ * 28px icon-button box kept the whole suite green — the two values the
10
+ * user explicitly asked for had no red-if-broken evidence). happy-dom
11
+ * has no layout engine, so — headerAvatarGeometry-style — these read the
12
+ * declarations themselves; they fail when the values move, disappear or
13
+ * stop reading the way they read here, and a deliberate rewrite is
14
+ * expected to update them.
15
+ */
16
+
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+
19
+ /** Declarations of the rule whose prelude IS `selector` (whole-prelude
20
+ * match, comments stripped), as [prop, value] pairs; null when absent. */
21
+ function ruleDeclarations(source: string, selector: string): Array<[string, string]> | null {
22
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "");
23
+ for (let at = code.indexOf(selector); at >= 0; at = code.indexOf(selector, at + 1)) {
24
+ const open = code.indexOf("{", at + selector.length);
25
+ if (open < 0) continue;
26
+ if (code.slice(at + selector.length, open).trim() !== "") continue;
27
+ let boundary = at - 1;
28
+ while (boundary >= 0 && !"}{;".includes(code[boundary])) boundary -= 1;
29
+ if (code.slice(boundary + 1, at).replace(/\s+/g, " ").trim() !== "") continue;
30
+ const body = code.slice(open + 1, code.indexOf("}", open));
31
+ const out: Array<[string, string]> = [];
32
+ for (const part of body.split(";")) {
33
+ const m = /^\s*([-a-zA-Z]+)\s*:\s*([\s\S]+?)\s*$/.exec(part);
34
+ if (m) out.push([m[1], m[2].replace(/\s+/g, " ")]);
35
+ }
36
+ return out;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ const adminTokens = readFileSync(join(here, "..", "styles", "admin-tokens.scss"), "utf8");
42
+ const iconButtonVars = readFileSync(join(here, "HkIconButtonVars.scss"), "utf8");
43
+ const iconButton = readFileSync(join(here, "HkIconButton.tsx"), "utf8");
44
+
45
+ describe("admin header chrome geometry (the two user-requested fixes)", () => {
46
+ it("keeps the console title at the panel's visual distance from the avatar (16px wing gap)", () => {
47
+ // The frontend's workspace button sits a flex gap PLUS its own 8px
48
+ // inline padding from the avatar (16px visual); the console title
49
+ // carried only the 8px flex gap and visibly hugged the avatar.
50
+ const rule = ruleDeclarations(adminTokens, ".s-admin-header-user");
51
+ expect(rule, ".s-admin-header-user must be declared").not.toBeNull();
52
+ expect(rule).toContainEqual(["gap", "var(--space-16)"]);
53
+ // Balanced left wing (the centre group stays viewport-centred).
54
+ expect(rule).toContainEqual(["flex", "1 1 0"]);
55
+ });
56
+
57
+ it("keeps the actions wing balanced (no margin-left:auto push) and the centre wing shrinkable", () => {
58
+ const actions = ruleDeclarations(adminTokens, ".s-admin-header-actions");
59
+ expect(actions, ".s-admin-header-actions must be declared").not.toBeNull();
60
+ expect(actions).toContainEqual(["flex", "1 1 0"]);
61
+ expect(actions).toContainEqual(["justify-content", "flex-end"]);
62
+ // The old push would fight the centre wing for the middle.
63
+ expect(actions).not.toContainEqual(["margin-left", "auto"]);
64
+ const centre = ruleDeclarations(adminTokens, ".s-admin-header-center");
65
+ expect(centre, ".s-admin-header-center must be declared").not.toBeNull();
66
+ expect(centre).toContainEqual(["flex", "0 1 auto"]);
67
+ });
68
+
69
+ it("keeps the 28px icon-button step at the theme toggle's own box", () => {
70
+ // HkThemeToggle's trigger buttons are 1.75rem (28px) boxes with 16px
71
+ // glyphs — the size any header action that must read as the same
72
+ // kind of button composes at.
73
+ const rule = ruleDeclarations(iconButtonVars, ".hk-icon-button-28");
74
+ expect(rule, ".hk-icon-button-28 must be declared").not.toBeNull();
75
+ expect(rule).toContainEqual(["--hi-icon-button-size", "28px"]);
76
+ // The component accepts the step.
77
+ expect(iconButton).toMatch(/PropType<16 \| 24 \| 28 \| 32 \| 36 \| 40>/);
78
+ });
79
+ });
@@ -378,7 +378,28 @@
378
378
  .s-admin-header-user {
379
379
  display: flex;
380
380
  align-items: center;
381
- gap: var(--space-8);
381
+ /* Avatar → title distance mirrors the chat frontend's header left
382
+ * wing: the panel's workspace button sits a flex gap PLUS its own
383
+ * 8px inline padding away from the avatar (16px visual), while the
384
+ * console title had only the flex gap — the console title visibly
385
+ * hugged the avatar (user report 2026-09-28: 后台标题离头像太近).
386
+ * The wing gap here carries the whole distance so the title text
387
+ * lands on the same rhythm as the panel's button content. */
388
+ gap: var(--space-16);
389
+ /* Balanced left wing (same grammar as ChatLayout's .s-header-bar-left):
390
+ * grows from the same flex basis as the actions wing so a centre
391
+ * group, when a page renders one, stays pinned to the true middle. */
392
+ flex: 1 1 0;
393
+ min-width: 0;
394
+ }
395
+
396
+ /* The page-owned centre group (filter tab strips): keeps its natural
397
+ * width and hands overflow to the HTabs scroller inside it. */
398
+ .s-admin-header-center {
399
+ flex: 0 1 auto;
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
382
403
  min-width: 0;
383
404
  }
384
405
 
@@ -430,8 +451,14 @@
430
451
  display: flex;
431
452
  align-items: center;
432
453
  gap: var(--space-6);
433
- margin-left: auto;
434
- flex-shrink: 0;
454
+ /* Balanced right wing: with the user wing at flex 1 1 0 the pair
455
+ * grows symmetrically, so the centre group stays centred and the
456
+ * actions still hug the right edge (justify-end replaces the old
457
+ * margin-left:auto push, which would fight the centre wing). The
458
+ * automatic min-content floor keeps the icon buttons from being
459
+ * squeezed below their own width on narrow viewports. */
460
+ flex: 1 1 0;
461
+ justify-content: flex-end;
435
462
  }
436
463
 
437
464
  .s-admin-header-action {