@celestia-island/hikari 0.55.322 → 0.55.323
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
|
@@ -50,6 +50,34 @@
|
|
|
50
50
|
font-weight: 600;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/* Optical vertical centering for the single-glyph fallback (2026-11-08
|
|
54
|
+
* user report: 头像首字母偏下). Flex centers the LINE box, but a capital
|
|
55
|
+
* glyph sits on the baseline — below the line box's geometric center on
|
|
56
|
+
* every UI font whose ascent exceeds its descent (Noto Sans CJK ≈0.065em,
|
|
57
|
+
* Windows Segoe UI ≈0.064em: ≈1px at md's 16px, visible in a circle).
|
|
58
|
+
* `text-box: trim-both cap alphabetic` trims the line box to
|
|
59
|
+
* [cap-top, baseline], so the centered box hugs the glyph ink — the
|
|
60
|
+
* avatar-initial case the property was designed for. Trim only reaches
|
|
61
|
+
* BLOCK-container line boxes (Chromium ignores it on a flex container's
|
|
62
|
+
* anonymous text block — verified headless), so under the guard the span
|
|
63
|
+
* swaps flex for block + align-content/text-align centering: same box,
|
|
64
|
+
* the trimmed line is centered on the block axis instead. Engines without
|
|
65
|
+
* text-box (pre-133 Chromium / pre-18.2 Safari / pre-154 Firefox) fail
|
|
66
|
+
* @supports and keep the flex anatomy unchanged — zero regression.
|
|
67
|
+
* CJK initials stay centered: the ideograph ink center lands on the
|
|
68
|
+
* [cap, baseline] band center (德 verified side by side, headless
|
|
69
|
+
* Chrome). Support note: the guard tests the `text-box` SHORTHAND,
|
|
70
|
+
* which Chromium parses from 133 (the longhands land in 128) — engines
|
|
71
|
+
* older than that fail @supports and keep the flex anatomy. */
|
|
72
|
+
@supports (text-box: trim-both cap alphabetic) {
|
|
73
|
+
.hk-avatar-fallback {
|
|
74
|
+
display: block;
|
|
75
|
+
align-content: center;
|
|
76
|
+
text-align: center;
|
|
77
|
+
text-box: trim-both cap alphabetic;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
[data-theme="dark"] .hk-avatar {
|
|
54
82
|
background-color: var(--hi-surface);
|
|
55
83
|
color: var(--hi-color-text-secondary, rgba(102,102,102,0.6));
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { compile } from "sass";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compiled-stylesheet guard for the avatar letter-fallback optical
|
|
10
|
+
* centering (2026-11-08 user report: 头像首字母偏下).
|
|
11
|
+
*
|
|
12
|
+
* Flex centering centers the LINE box, but a capital glyph sits on the
|
|
13
|
+
* baseline — below the line box's geometric center on every UI font
|
|
14
|
+
* whose ascent exceeds its descent (Noto Sans CJK ≈0.065em, Windows
|
|
15
|
+
* Segoe UI ≈0.064em). The fix trims the line box to [cap-top, baseline]
|
|
16
|
+
* with `text-box: trim-both cap alphabetic` under an @supports guard
|
|
17
|
+
* (trim only reaches BLOCK-container line boxes — Chromium ignores it on
|
|
18
|
+
* a flex container's anonymous text block, verified headless — so the
|
|
19
|
+
* height:100% fallbacks swap flex for block + align-content centering
|
|
20
|
+
* under the guard; the auto-height menu fallback only adds the trim).
|
|
21
|
+
*
|
|
22
|
+
* Vitest stubs CSS, so the runtime suite can never catch a lost rule;
|
|
23
|
+
* this guard pins the COMPILED sheets. All three sheets are mirrored
|
|
24
|
+
* verbatim into the Rust crate (vue-mirror drift tests), which catches
|
|
25
|
+
* one-sided edits — this file catches the block disappearing from BOTH
|
|
26
|
+
* copies at once. Supported engines: Chromium 133+, Safari 18.2+,
|
|
27
|
+
* Firefox 154+ (the guard tests the text-box shorthand; the longhands
|
|
28
|
+
* exist in Chromium from 128) — anything older fails @supports and keeps
|
|
29
|
+
* the flex anatomy (the pre-fix behavior), which is why the base rules
|
|
30
|
+
* below are also pinned to keep their flex centering.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
34
|
+
|
|
35
|
+
const SUPPORTS_PREAMBLE = "@supports (text-box: trim-both cap alphabetic)";
|
|
36
|
+
|
|
37
|
+
const css = {
|
|
38
|
+
avatar: compile(resolve(componentDir, "HkAvatar.scss"), { style: "expanded" }).css,
|
|
39
|
+
menuIdentity: compile(resolve(componentDir, "HkMenuIdentityItem.scss"), { style: "expanded" }).css,
|
|
40
|
+
adminTokens: compile(resolve(componentDir, "../styles/admin-tokens.scss"), { style: "expanded" }).css,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The body of the FIRST @supports block with exactly `preamble`, brace-matched. */
|
|
44
|
+
function supportsBody(sheetCss: string, preamble: string, label: string): string {
|
|
45
|
+
const start = sheetCss.indexOf(preamble);
|
|
46
|
+
expect(start, `${label}: ${preamble} block present in the compiled sheet`).toBeGreaterThanOrEqual(0);
|
|
47
|
+
let depth = 0;
|
|
48
|
+
let open = -1;
|
|
49
|
+
for (let at = start + preamble.length; at < sheetCss.length; at += 1) {
|
|
50
|
+
const ch = sheetCss[at];
|
|
51
|
+
if (ch === "{") {
|
|
52
|
+
if (open < 0) open = at;
|
|
53
|
+
depth += 1;
|
|
54
|
+
} else if (ch === "}") {
|
|
55
|
+
depth -= 1;
|
|
56
|
+
if (depth === 0) return sheetCss.slice(open + 1, at);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`${label}: unterminated @supports block`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The prelude of the FIRST rule inside the first `@supports` block with
|
|
63
|
+
* exactly `preamble` — the selector the guard actually targets. */
|
|
64
|
+
function guardedSelector(sheetCss: string, preamble: string, label: string): string {
|
|
65
|
+
const body = supportsBody(sheetCss, preamble, label);
|
|
66
|
+
const open = body.indexOf("{");
|
|
67
|
+
return body.slice(0, open).trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The body of the top-level rule whose prelude is exactly `selector`. */
|
|
71
|
+
function ruleFor(sheetCss: string, selector: string, label: string): string {
|
|
72
|
+
const needle = `${selector} {`;
|
|
73
|
+
const start = sheetCss.indexOf(needle);
|
|
74
|
+
expect(start, `${label}: ${selector} rule present in the compiled sheet`).toBeGreaterThanOrEqual(0);
|
|
75
|
+
const close = sheetCss.indexOf("}", start);
|
|
76
|
+
return sheetCss.slice(start, close + 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("avatar letter-fallback optical centering (text-box guard)", () => {
|
|
80
|
+
it("positive control: the extractor finds the known base anatomy", () => {
|
|
81
|
+
// If this stops matching, ruleFor/supportsBody are broken — fix the
|
|
82
|
+
// extractor, not the assertions below.
|
|
83
|
+
expect(ruleFor(css.avatar, ".hk-avatar", "avatar")).toContain("align-items: center");
|
|
84
|
+
expect(ruleFor(css.adminTokens, ".s-user-avatar", "admin-tokens")).toContain("display: flex");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("hk-avatar-fallback: guard block swaps flex for block + centered trimmed line", () => {
|
|
88
|
+
const body = supportsBody(css.avatar, SUPPORTS_PREAMBLE, "avatar");
|
|
89
|
+
expect(guardedSelector(css.avatar, SUPPORTS_PREAMBLE, "avatar")).toBe(".hk-avatar-fallback");
|
|
90
|
+
expect(body).toContain("display: block");
|
|
91
|
+
expect(body).toContain("align-content: center");
|
|
92
|
+
expect(body).toContain("text-align: center");
|
|
93
|
+
expect(body).toContain("text-box: trim-both cap alphabetic");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("hk-avatar-fallback: base rule keeps the flex anatomy (no-trim engines)", () => {
|
|
97
|
+
const rule = ruleFor(css.avatar, ".hk-avatar-fallback", "avatar");
|
|
98
|
+
expect(rule).toContain("display: flex");
|
|
99
|
+
expect(rule).toContain("align-items: center");
|
|
100
|
+
expect(rule).toContain("justify-content: center");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("s-user-avatar: guard block swaps flex for block + centered trimmed line", () => {
|
|
104
|
+
const body = supportsBody(css.adminTokens, SUPPORTS_PREAMBLE, "admin-tokens");
|
|
105
|
+
expect(guardedSelector(css.adminTokens, SUPPORTS_PREAMBLE, "admin-tokens")).toBe(".s-user-avatar");
|
|
106
|
+
expect(body).toContain("display: block");
|
|
107
|
+
expect(body).toContain("align-content: center");
|
|
108
|
+
expect(body).toContain("text-align: center");
|
|
109
|
+
expect(body).toContain("text-box: trim-both cap alphabetic");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("s-user-avatar: base rule keeps the flex anatomy (no-trim engines)", () => {
|
|
113
|
+
const rule = ruleFor(css.adminTokens, ".s-user-avatar", "admin-tokens");
|
|
114
|
+
expect(rule).toContain("display: flex");
|
|
115
|
+
expect(rule).toContain("align-items: center");
|
|
116
|
+
expect(rule).toContain("justify-content: center");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("menu identity fallback: trim only — auto-height flex item needs no anatomy swap", () => {
|
|
120
|
+
const body = supportsBody(css.menuIdentity, SUPPORTS_PREAMBLE, "menu-identity");
|
|
121
|
+
expect(guardedSelector(css.menuIdentity, SUPPORTS_PREAMBLE, "menu-identity")).toBe(
|
|
122
|
+
".hk-menu-identity-item__fallback",
|
|
123
|
+
);
|
|
124
|
+
expect(body).toContain("text-box: trim-both cap alphabetic");
|
|
125
|
+
expect(body).not.toContain("display:");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("avatar letter-fallback sheets stay mirrored", () => {
|
|
130
|
+
// The Rust crate compiles vendored copies of these sheets; the drift
|
|
131
|
+
// tests over there fail on one-sided edits. This re-reads the SOURCE
|
|
132
|
+
// files to pin the mirror files' presence next to the pinned behavior,
|
|
133
|
+
// so deleting the guard from BOTH copies cannot slip through silently
|
|
134
|
+
// (drift tests compare copies — they stay green when both sides lose
|
|
135
|
+
// the block together).
|
|
136
|
+
it("vendored mirror copies still carry the guard (double-sided delete tripwire)", () => {
|
|
137
|
+
const crateRoot = resolve(componentDir, "../../../components/src/styles/vue-mirror");
|
|
138
|
+
for (const rel of [
|
|
139
|
+
"components/HkAvatar.scss",
|
|
140
|
+
"components/HkMenuIdentityItem.scss",
|
|
141
|
+
"styles/admin-tokens.scss",
|
|
142
|
+
]) {
|
|
143
|
+
const mirror = readFileSync(resolve(crateRoot, rel), "utf8");
|
|
144
|
+
expect(mirror, `${rel} mirror carries the guard`).toContain("text-box: trim-both cap alphabetic");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
color: rgb(var(--color-primary) / 70%);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/* The fallback span is a blockified flex item with auto height, so flex
|
|
46
|
+
centering of the trimmed box is exact — no anatomy swap needed here
|
|
47
|
+
(the height:100% fallbacks swap flex for block under the same guard;
|
|
48
|
+
full rationale: HkAvatar.scss). */
|
|
49
|
+
@supports (text-box: trim-both cap alphabetic) {
|
|
50
|
+
.hk-menu-identity-item__fallback {
|
|
51
|
+
text-box: trim-both cap alphabetic;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
.hk-menu-identity-item__text {
|
|
46
56
|
display: flex;
|
|
47
57
|
flex-direction: column;
|
|
@@ -897,6 +897,22 @@
|
|
|
897
897
|
letter-spacing: 0.02em;
|
|
898
898
|
}
|
|
899
899
|
|
|
900
|
+
/* Optical vertical centering for the letter fallback (2026-11-08 user
|
|
901
|
+
report: 头像首字母偏下) — same mechanism and guard as HkAvatar.scss:
|
|
902
|
+
flex centers the line box while the capital sits on the baseline
|
|
903
|
+
(≈0.065em low on CJK-metric fonts), and trim only reaches block
|
|
904
|
+
containers, so under @supports the span swaps flex for
|
|
905
|
+
block + align-content/text-align centering; unsupported engines keep
|
|
906
|
+
the flex anatomy unchanged. */
|
|
907
|
+
@supports (text-box: trim-both cap alphabetic) {
|
|
908
|
+
.s-user-avatar {
|
|
909
|
+
display: block;
|
|
910
|
+
align-content: center;
|
|
911
|
+
text-align: center;
|
|
912
|
+
text-box: trim-both cap alphabetic;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
900
916
|
.s-user-avatar-img {
|
|
901
917
|
width: 100%;
|
|
902
918
|
height: 100%;
|