@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 +1 -1
- package/src/components/HkAdminHeader.test.tsx +19 -0
- package/src/components/HkAdminHeader.tsx +13 -0
- package/src/components/HkIconButton.tsx +6 -1
- package/src/components/HkIconButtonVars.scss +5 -0
- package/src/components/adminHeaderChrome.test.ts +79 -0
- package/src/styles/admin-tokens.scss +30 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
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 {
|