@celestia-island/hikari 0.55.312 → 0.55.314
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/animation/registerAnimations.ts +4 -0
- package/src/components/HkBadge.scss +23 -0
- package/src/components/HkBadge.test.tsx +71 -0
- package/src/components/HkBadge.tsx +4 -0
- package/src/components/HkErrorLanding.scss +2 -1
- package/src/components/HkModal.leavepin.test.tsx +53 -12
- package/src/components/HkModal.stepflowswap.test.tsx +19 -7
- package/src/components/HkModal.tsx +40 -31
- package/src/components/HkSplash.scss +3 -1
- package/src/components/HkTitleBar.scss +5 -1
- package/src/composables/useSizeMorph.ts +35 -24
- package/src/plugins/bootLandingCss.mjs +1 -1
- package/src/plugins/bootLandingCss.test.ts +7 -0
- package/src/scale.scss +1 -1
- package/src/styles/theme/scale.scss +1 -1
- package/src/theme/fontStacks.contract.test.ts +98 -0
package/package.json
CHANGED
|
@@ -143,3 +143,7 @@ registerCssAnimation("hk-screen-identify-in");
|
|
|
143
143
|
// brushed window still reads as live. Reduced motion keeps the dashed
|
|
144
144
|
// pending look and drops the loop (house pattern, see HkSkeleton).
|
|
145
145
|
registerCssAnimation("hk-range-ribbon-pulse", { infinite: true });
|
|
146
|
+
// HkBadge's presence pulse (opacity-only, loops forever): the `pulse`
|
|
147
|
+
// prop's dot breathes while its state is in-flight — a settled dot sits
|
|
148
|
+
// static. Reduced motion drops the loop (house pattern, see HkSkeleton).
|
|
149
|
+
registerCssAnimation("hk-badge-dot-pulse", { infinite: true });
|
|
@@ -64,6 +64,29 @@
|
|
|
64
64
|
flex-shrink: 0;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Presence pulse (data-pulse, dot required): an in-flight / live state
|
|
68
|
+
breathes instead of sitting static. Opacity-only so the pill's layout
|
|
69
|
+
box never moves; consumers wanting a glow layer their own shadow. */
|
|
70
|
+
@keyframes hk-badge-dot-pulse {
|
|
71
|
+
0%,
|
|
72
|
+
100% {
|
|
73
|
+
opacity: 1;
|
|
74
|
+
}
|
|
75
|
+
50% {
|
|
76
|
+
opacity: 0.4;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.hk-badge[data-pulse] [data-el="dot"] {
|
|
81
|
+
animation: hk-badge-dot-pulse 1.6s ease-in-out infinite;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (prefers-reduced-motion: reduce) {
|
|
85
|
+
.hk-badge[data-pulse] [data-el="dot"] {
|
|
86
|
+
animation: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
/* dot-only: strip the pill padding/border/background so it's just the
|
|
68
91
|
indicator dot, not a dot wrapped in a bordered pill. */
|
|
69
92
|
.hk-badge[data-dot-only] {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { compile } from "sass";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import HkBadge from "./HkBadge";
|
|
9
|
+
|
|
10
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
11
|
+
|
|
12
|
+
function mount(renderNode: () => ReturnType<typeof h>) {
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
document.body.appendChild(container);
|
|
15
|
+
const app = createApp({ render: renderNode });
|
|
16
|
+
app.mount(container);
|
|
17
|
+
mounts.push({ app, container });
|
|
18
|
+
return container;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
23
|
+
app.unmount();
|
|
24
|
+
container.remove();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Vitest stubs CSS, so the runtime suite can never catch a lost rule —
|
|
29
|
+
// the pulse animation contract is additionally pinned by the compiled
|
|
30
|
+
// stylesheet guard at the bottom of this file (HkCornerBadge pattern).
|
|
31
|
+
describe("HkBadge pulse", () => {
|
|
32
|
+
it("renders the dot and data-pulse only when both dot and pulse are set", () => {
|
|
33
|
+
const container = mount(() => h(HkBadge, { dot: true, pulse: true }, () => "Creating"));
|
|
34
|
+
expect(container.querySelector(".hk-badge [data-el='dot']")).toBeTruthy();
|
|
35
|
+
expect(container.querySelector(".hk-badge[data-pulse]")).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("never sets data-pulse without a dot — a pulsing pill with no dot is meaningless", () => {
|
|
39
|
+
const container = mount(() => h(HkBadge, { pulse: true }, () => "Creating"));
|
|
40
|
+
expect(container.querySelector(".hk-badge")).toBeTruthy();
|
|
41
|
+
expect(container.querySelector("[data-pulse]")).toBeNull();
|
|
42
|
+
expect(container.querySelector("[data-el='dot']")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("keeps the static dot the default — pulse is opt-in per call site", () => {
|
|
46
|
+
const container = mount(() => h(HkBadge, { dot: true }, () => "Idle"));
|
|
47
|
+
expect(container.querySelector(".hk-badge [data-el='dot']")).toBeTruthy();
|
|
48
|
+
expect(container.querySelector("[data-pulse]")).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("HkBadge pulse stylesheet contract", () => {
|
|
53
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const css = compile(resolve(here, "HkBadge.scss"), { style: "expanded" }).css;
|
|
55
|
+
|
|
56
|
+
it("drives the pulse through the data-pulse selector on the dot only", () => {
|
|
57
|
+
// Sass compiles quoted attribute selectors unquoted ([data-el=dot]).
|
|
58
|
+
expect(css).toContain(".hk-badge[data-pulse] [data-el=dot]");
|
|
59
|
+
// The animation must never ride the pill itself — layout stays frozen.
|
|
60
|
+
expect(css.match(/\.hk-badge\s*\{[^}]*animation/)).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("names the keyframes and runs them infinitely", () => {
|
|
64
|
+
expect(css).toContain("@keyframes hk-badge-dot-pulse");
|
|
65
|
+
expect(/\.hk-badge\[data-pulse\] \[data-el=dot\]\s*\{[^}]*animation:\s*hk-badge-dot-pulse[^;]*infinite/.test(css)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("switches the pulse off under prefers-reduced-motion", () => {
|
|
69
|
+
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)\s*\{[^@]*\.hk-badge\[data-pulse\] \[data-el=dot\]\s*\{[^}]*animation:\s*none/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -15,6 +15,9 @@ export default defineComponent({
|
|
|
15
15
|
props: {
|
|
16
16
|
variant: { type: String as PropType<BadgeVariant>, default: "default" },
|
|
17
17
|
dot: { type: Boolean, default: false },
|
|
18
|
+
/** Animate the dot with a presence pulse (in-flight / live state).
|
|
19
|
+
* Meaningful only with `dot`; honors prefers-reduced-motion. */
|
|
20
|
+
pulse: { type: Boolean, default: false },
|
|
18
21
|
size: { type: String as PropType<"sm" | "md">, default: "md" },
|
|
19
22
|
mono: { type: Boolean, default: false },
|
|
20
23
|
uppercase: { type: Boolean, default: false },
|
|
@@ -38,6 +41,7 @@ export default defineComponent({
|
|
|
38
41
|
data-size={props.size}
|
|
39
42
|
data-pill={props.pill ? undefined : "false"}
|
|
40
43
|
data-mono={props.mono ? "" : undefined}
|
|
44
|
+
data-pulse={props.pulse && props.dot ? "" : undefined}
|
|
41
45
|
data-uppercase={props.uppercase ? "" : undefined}
|
|
42
46
|
data-dot-only={(props.dot && !slots.default) || undefined}
|
|
43
47
|
style={Object.keys(style.value).length ? style.value : undefined}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// host themes (dark mode etc.) when the vars exist.
|
|
7
7
|
|
|
8
8
|
@use "../styles/polyfill" as *;
|
|
9
|
+
@use "./hikari-vars" as vars;
|
|
9
10
|
|
|
10
11
|
.hk-error-landing {
|
|
11
12
|
--hel-background: var(--color-background, 246 247 249);
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
padding: var(--space-20, 1.25rem);
|
|
35
36
|
background: rgb(var(--hel-background));
|
|
36
37
|
color: rgb(var(--hel-text));
|
|
37
|
-
font-family: var(--font-sans,
|
|
38
|
+
font-family: var(--font-sans, vars.$hikari-font-family-sans);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
.hk-error-landing__card {
|
|
@@ -15,7 +15,7 @@ import HkModal from "./HkModal";
|
|
|
15
15
|
*
|
|
16
16
|
* happy-dom has no layout engine, so the frame's offsetHeight is stubbed
|
|
17
17
|
* at the HTMLElement prototype level BEFORE mount — the morph's
|
|
18
|
-
*
|
|
18
|
+
* open-edge arm then measures 500px and pins it, and every
|
|
19
19
|
* mid-leave assertion reads the inline height string directly.
|
|
20
20
|
*/
|
|
21
21
|
const mounts: ReturnType<typeof createApp>[] = [];
|
|
@@ -128,7 +128,7 @@ describe("HkModal leave-window height pin", () => {
|
|
|
128
128
|
try {
|
|
129
129
|
const rig = await mountRig();
|
|
130
130
|
|
|
131
|
-
// Open to rest: the
|
|
131
|
+
// Open to rest: the open-edge arm measured 500px and pinned it.
|
|
132
132
|
rig.open.value = true;
|
|
133
133
|
await vi.advanceTimersByTimeAsync(600);
|
|
134
134
|
expect(rig.frameEl()).not.toBeNull();
|
|
@@ -157,7 +157,7 @@ describe("HkModal leave-window height pin", () => {
|
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("
|
|
160
|
+
it("leaves the frame unpinned through the enter; the open edge pins the settled height", async () => {
|
|
161
161
|
vi.useFakeTimers();
|
|
162
162
|
freezeRaf();
|
|
163
163
|
stubPhoneViewport();
|
|
@@ -166,24 +166,65 @@ describe("HkModal leave-window height pin", () => {
|
|
|
166
166
|
try {
|
|
167
167
|
const rig = await mountRig();
|
|
168
168
|
|
|
169
|
-
// Open: the enter
|
|
169
|
+
// Open: nothing arms during the enter — the frame stays height:auto
|
|
170
|
+
// so content that streams in mid-enter reflows it live instead of
|
|
171
|
+
// sitting clipped behind a mount-time pin (2026-11-07 chest report:
|
|
172
|
+
// topic windows fetched their detail mid-enter and the bottom of
|
|
173
|
+
// the window stayed missing for the whole open animation).
|
|
170
174
|
rig.open.value = true;
|
|
171
175
|
await vi.advanceTimersByTimeAsync(150); // mid-enter, past the flip
|
|
172
|
-
expect(rig.
|
|
176
|
+
expect(rig.frameEl()).not.toBeNull();
|
|
177
|
+
expect(rig.pinHeight()).toBe("");
|
|
173
178
|
|
|
174
|
-
// Content grows mid-enter —
|
|
175
|
-
//
|
|
176
|
-
// stays put; in a real browser the RO fires and is deferred too).
|
|
179
|
+
// Content grows mid-enter: still unpinned — the growth IS the
|
|
180
|
+
// frame (auto height follows it frame-by-frame).
|
|
177
181
|
layoutH = 700;
|
|
178
182
|
await vi.advanceTimersByTimeAsync(50);
|
|
179
|
-
expect(rig.pinHeight()).toBe("
|
|
183
|
+
expect(rig.pinHeight()).toBe("");
|
|
180
184
|
|
|
181
|
-
// The open edge
|
|
182
|
-
//
|
|
185
|
+
// The open edge pins the settled natural height directly — no
|
|
186
|
+
// deferred flush, no second growth spurt after the enter.
|
|
183
187
|
await vi.advanceTimersByTimeAsync(450);
|
|
184
188
|
expect(rig.pinHeight()).toBe("700px");
|
|
185
189
|
|
|
186
|
-
// Sanity: the surface really rested open before the
|
|
190
|
+
// Sanity: the surface really rested open before the pin assertion.
|
|
191
|
+
expect(rig.frameEl()).not.toBeNull();
|
|
192
|
+
} finally {
|
|
193
|
+
restoreLayout();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("drops the held pin at a reopen-interrupt edge; the open edge re-pins", async () => {
|
|
198
|
+
vi.useFakeTimers();
|
|
199
|
+
freezeRaf();
|
|
200
|
+
stubPhoneViewport();
|
|
201
|
+
stubDurations();
|
|
202
|
+
stubLayout(500);
|
|
203
|
+
try {
|
|
204
|
+
const rig = await mountRig();
|
|
205
|
+
|
|
206
|
+
// Open to rest (the open-edge arm pins 500px), then request a
|
|
207
|
+
// close: the leave holds the pin (the fold owns the frame).
|
|
208
|
+
rig.open.value = true;
|
|
209
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
210
|
+
expect(rig.pinHeight()).toBe("500px");
|
|
211
|
+
rig.open.value = false;
|
|
212
|
+
await vi.advanceTimersByTimeAsync(150); // mid-leave
|
|
213
|
+
expect(rig.frameEl()).not.toBeNull();
|
|
214
|
+
expect(rig.pinHeight()).toBe("500px");
|
|
215
|
+
|
|
216
|
+
// Reopen mid-leave: the reopening edge must DROP the held pin —
|
|
217
|
+
// otherwise the reopen enter plays at the OLD content's height and
|
|
218
|
+
// clips whatever the new content renders (the sibling of the
|
|
219
|
+
// first-open bug this suite retired).
|
|
220
|
+
rig.open.value = true;
|
|
221
|
+
await vi.advanceTimersByTimeAsync(150); // mid-reopen-enter
|
|
222
|
+
expect(rig.frameEl()).not.toBeNull();
|
|
223
|
+
expect(rig.pinHeight()).toBe("");
|
|
224
|
+
|
|
225
|
+
// The open edge re-pins at rest (content unchanged → same height).
|
|
226
|
+
await vi.advanceTimersByTimeAsync(450);
|
|
227
|
+
expect(rig.pinHeight()).toBe("500px");
|
|
187
228
|
expect(rig.frameEl()).not.toBeNull();
|
|
188
229
|
} finally {
|
|
189
230
|
restoreLayout();
|
|
@@ -456,14 +456,21 @@ describe("HkModal rides its chrome and gates the slide window", () => {
|
|
|
456
456
|
stubClip();
|
|
457
457
|
stubBox(500, Number.POSITIVE_INFINITY);
|
|
458
458
|
stubPhoneViewport();
|
|
459
|
-
const roInstances: Array<{ callback: () => void }> = [];
|
|
459
|
+
const roInstances: Array<{ callback: () => void; target: Element | null }> = [];
|
|
460
|
+
// Select the morph's observer by its OBSERVED TARGET, never by index:
|
|
461
|
+
// the overlay scrollbar attaches its own ResizeObserver at the mounted
|
|
462
|
+
// flip, and since the morph arms at the `open` edge (after the enter)
|
|
463
|
+
// the scrollbar's observer registers first.
|
|
464
|
+
const morphRo = () =>
|
|
465
|
+
roInstances.find((ro) => ro.target?.classList.contains("hk-modal-body-inner"))!;
|
|
460
466
|
vi.stubGlobal("ResizeObserver", class {
|
|
461
467
|
callback: () => void;
|
|
468
|
+
target: Element | null = null;
|
|
462
469
|
constructor(cb: () => void) {
|
|
463
470
|
this.callback = cb;
|
|
464
471
|
roInstances.push(this);
|
|
465
472
|
}
|
|
466
|
-
observe() {}
|
|
473
|
+
observe(el: Element) { this.target = el; }
|
|
467
474
|
disconnect() {}
|
|
468
475
|
});
|
|
469
476
|
const rig = await mountRig();
|
|
@@ -477,7 +484,7 @@ describe("HkModal rides its chrome and gates the slide window", () => {
|
|
|
477
484
|
rig.announceSwap({ delta: 0, durationMs: 300, phase: "swap" });
|
|
478
485
|
await nextTick();
|
|
479
486
|
content = 560;
|
|
480
|
-
|
|
487
|
+
morphRo().callback();
|
|
481
488
|
await vi.advanceTimersByTimeAsync(400);
|
|
482
489
|
expect(frame.style.height).toBe("596px");
|
|
483
490
|
|
|
@@ -506,14 +513,19 @@ describe("HkModal rides its chrome and gates the slide window", () => {
|
|
|
506
513
|
stubClip();
|
|
507
514
|
stubBox(500, Number.POSITIVE_INFINITY);
|
|
508
515
|
stubPhoneViewport();
|
|
509
|
-
const roInstances: Array<{ callback: () => void }> = [];
|
|
516
|
+
const roInstances: Array<{ callback: () => void; target: Element | null }> = [];
|
|
517
|
+
// Same target-based selection as the safety-timer test: the scrollbar's
|
|
518
|
+
// observer registers before the morph's open-edge arm.
|
|
519
|
+
const morphRo = () =>
|
|
520
|
+
roInstances.find((ro) => ro.target?.classList.contains("hk-modal-body-inner"))!;
|
|
510
521
|
vi.stubGlobal("ResizeObserver", class {
|
|
511
522
|
callback: () => void;
|
|
523
|
+
target: Element | null = null;
|
|
512
524
|
constructor(cb: () => void) {
|
|
513
525
|
this.callback = cb;
|
|
514
526
|
roInstances.push(this);
|
|
515
527
|
}
|
|
516
|
-
observe() {}
|
|
528
|
+
observe(el: Element) { this.target = el; }
|
|
517
529
|
disconnect() {}
|
|
518
530
|
});
|
|
519
531
|
// No rAF stub: happy-dom maps rAF onto its timer queue, so the
|
|
@@ -528,7 +540,7 @@ describe("HkModal rides its chrome and gates the slide window", () => {
|
|
|
528
540
|
rig.announceSwap({ delta: 0, durationMs: 300, phase: "swap" });
|
|
529
541
|
await nextTick();
|
|
530
542
|
content = 560;
|
|
531
|
-
|
|
543
|
+
morphRo().callback();
|
|
532
544
|
await vi.advanceTimersByTimeAsync(700);
|
|
533
545
|
expect(frame.style.height).toBe("596px");
|
|
534
546
|
|
|
@@ -540,7 +552,7 @@ describe("HkModal rides its chrome and gates the slide window", () => {
|
|
|
540
552
|
// Harness validity + unfreeze proof: the SAME observer path now
|
|
541
553
|
// moves the pin without any event.
|
|
542
554
|
content = 700;
|
|
543
|
-
|
|
555
|
+
morphRo().callback();
|
|
544
556
|
await vi.advanceTimersByTimeAsync(700);
|
|
545
557
|
expect(frame.style.height).toBe("796px");
|
|
546
558
|
restoreBox();
|
|
@@ -284,24 +284,27 @@ export default defineComponent({
|
|
|
284
284
|
const innerRef = ref<HTMLElement>();
|
|
285
285
|
// Content-driven size morphing: the frame follows content growth with
|
|
286
286
|
// the height transition instead of snapping (see useSizeMorph). The
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
287
|
+
// morph arms at the machine's `open` edge only — never during the
|
|
288
|
+
// enter. Under the current whole-window zoom+rise choreography the
|
|
289
|
+
// open animation is opacity+transform on the frame: nothing about it
|
|
290
|
+
// reads the frame's height, so an enter-time pin has nothing left to
|
|
291
|
+
// protect, and pinning during the enter would freeze the frame at the
|
|
292
|
+
// mount-time height while `overflow: hidden` clips whatever content
|
|
293
|
+
// streams in behind it (2026-11-07 chest report — topic windows
|
|
294
|
+
// fetched their detail mid-enter and read as "the bottom of the
|
|
295
|
+
// window is missing" for the whole open animation, then grew). The
|
|
296
|
+
// old arm-and-freeze existed for the retired height-relative unveil
|
|
297
|
+
// (translateY fraction + bottom clip of the frame height), whose
|
|
298
|
+
// geometry a mid-enter resize would have corrupted; that grammar is
|
|
299
|
+
// gone, so the freeze is gone with it.
|
|
300
|
+
// The freeze flag (round 16) stays: while a stepflow SLIDE is in
|
|
301
|
+
// flight the sheet's height must not chase the flow's changing cell —
|
|
302
|
+
// the gate below also returns true then, and the morph window's own
|
|
303
|
+
// event flushes the frozen change with an explicit remeasure.
|
|
298
304
|
let swapFrozen = false;
|
|
299
305
|
let swapFreezeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
300
306
|
const morph = useSizeMorph(contentRef, innerRef, {
|
|
301
|
-
deferRemeasure: () =>
|
|
302
|
-
swapFrozen ||
|
|
303
|
-
machine.phase.value === "openingFrom" ||
|
|
304
|
-
machine.phase.value === "openingTo",
|
|
307
|
+
deferRemeasure: () => swapFrozen,
|
|
305
308
|
// The sheet's chrome and content block ride the fold edge in
|
|
306
309
|
// lockstep with the clip sweep: the title bar glides with the edge
|
|
307
310
|
// instead of being sliced by it mid-sweep, the content block starts
|
|
@@ -593,26 +596,32 @@ export default defineComponent({
|
|
|
593
596
|
backGuard.push();
|
|
594
597
|
}
|
|
595
598
|
surfTrack.run();
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
// the
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
599
|
+
// No morph arm here, on purpose: the enter is opacity+transform
|
|
600
|
+
// (whole-window zoom+rise), which never reads the frame's
|
|
601
|
+
// height — the frame stays at `height: auto` so content that
|
|
602
|
+
// streams in mid-enter reflows it live instead of being clipped
|
|
603
|
+
// behind a stale mount-time pin (see the morph gate above). The
|
|
604
|
+
// size morph arms once, at the `open` edge below.
|
|
605
|
+
// A REOPEN-INTERRUPT (OPEN mid-close) lands here too: the close
|
|
606
|
+
// request held the previous cycle's pin (morph.hold(), so the
|
|
607
|
+
// fold played on a stable box) and nothing else would drop it —
|
|
608
|
+
// the reopen enter would then play pinned at the OLD content's
|
|
609
|
+
// height, clipping new content exactly like the first-open bug
|
|
610
|
+
// this file retired. Release the pin so every enter — first or
|
|
611
|
+
// interrupted — starts at `height: auto`; the hold-kept pin's
|
|
612
|
+
// other purpose (the close completing) is served by the
|
|
613
|
+
// finalize edge's own release.
|
|
614
|
+
morph.release();
|
|
607
615
|
} else if (to === "open") {
|
|
608
616
|
surfTrack.cancel();
|
|
609
617
|
onAfterEnter();
|
|
610
|
-
//
|
|
618
|
+
// The ONLY morph arm (see the openingFrom note): the enter has
|
|
619
|
+
// landed, so calibrating and pinning here measures the frame at
|
|
620
|
+
// rest — with the enter classes no longer in play one tick
|
|
621
|
+
// later, the follow-up remeasure re-checks the natural height
|
|
622
|
+
// under the base height transition (a growth that landed in the
|
|
623
|
+
// last tick of the enter animates instead of snapping).
|
|
611
624
|
morph.start();
|
|
612
|
-
// Flush the growth deferred through the unfold. One tick later
|
|
613
|
-
// the transition classes are off the frame, so the base height
|
|
614
|
-
// transition animates the pin change instead of the enter's
|
|
615
|
-
// opacity/transform-only transition list swallowing it as a snap.
|
|
616
625
|
void nextTick(() => {
|
|
617
626
|
if (machine.phase.value === "open") morph.remeasure();
|
|
618
627
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@use "./hikari-vars" as vars;
|
|
2
|
+
|
|
1
3
|
.hk-splash {
|
|
2
4
|
display: flex;
|
|
3
5
|
flex-direction: column;
|
|
@@ -6,7 +8,7 @@
|
|
|
6
8
|
min-height: 100vh;
|
|
7
9
|
background: var(--hi-color-bg-canvas, #0d1117);
|
|
8
10
|
color: var(--hi-color-text-primary, #1e1e1e);
|
|
9
|
-
font-family: var(--font-sans, -
|
|
11
|
+
font-family: var(--font-sans, vars.$hikari-font-family-sans);
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
.hk-splash-main {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Text selection is chrome-disabled across the whole bar.
|
|
7
7
|
|
|
8
8
|
@use "../styles/polyfill" as *;
|
|
9
|
+
@use "./hikari-vars" as vars;
|
|
9
10
|
|
|
10
11
|
.hk-titlebar {
|
|
11
12
|
--hk-tb-height: 32px;
|
|
@@ -42,7 +43,10 @@
|
|
|
42
43
|
background: var(--hk-tb-bg);
|
|
43
44
|
@include backdrop-filter(blur(var(--blur-md, 16px)));
|
|
44
45
|
border-bottom: 1px solid var(--hk-tb-border);
|
|
45
|
-
|
|
46
|
+
// The token, not a literal (docs/en/guides/fonts.md): this sheet used
|
|
47
|
+
// to hand-roll a CJK-less sans, dropping CJK titlebar text onto a
|
|
48
|
+
// platform fallback on Linux.
|
|
49
|
+
font-family: var(--font-sans, vars.$hikari-font-family-sans);
|
|
46
50
|
-webkit-app-region: drag;
|
|
47
51
|
app-region: drag;
|
|
48
52
|
}
|
|
@@ -34,10 +34,13 @@ const REVEAL_WARMUP_FRAMES = 2;
|
|
|
34
34
|
export interface SizeMorph {
|
|
35
35
|
/** Arm the morph: observe the content and pin the frame's natural
|
|
36
36
|
* height on every change. Call once the surface finished its open
|
|
37
|
-
* enter
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* enter — the enter must own the frame's geometry (HkModal and
|
|
38
|
+
* HkSelectPanel both arm at the machine's `open` edge; the enter
|
|
39
|
+
* classes are off the frame by then, so calibration reads the rest
|
|
40
|
+
* state). There is no early-arm protocol anymore: the modal's enter
|
|
41
|
+
* is opacity+transform and never reads the frame's height, so an
|
|
42
|
+
* enter-time pin would only freeze a stale height under
|
|
43
|
+
* `overflow: hidden`. */
|
|
41
44
|
start(): void;
|
|
42
45
|
/** Disarm the morph and release the frame to `height: auto` — call
|
|
43
46
|
* before a surface's leave/close so the exit animation owns the
|
|
@@ -48,12 +51,20 @@ export interface SizeMorph {
|
|
|
48
51
|
* choreography owns the frame's geometry through the whole leave and
|
|
49
52
|
* the pin keeps it stable (a mid-leave content change must not resize
|
|
50
53
|
* the folding frame). The next start() re-arms and, because the pin
|
|
51
|
-
* was kept, a re-pin to the SAME natural height is a no-op (
|
|
52
|
-
*
|
|
54
|
+
* was kept, a re-pin to the SAME natural height is a no-op (a reopen
|
|
55
|
+
* that goes hold → start without releasing — no current HkModal path
|
|
56
|
+
* does this, its reopen edge releases the pin first; held content is unchanged, delta
|
|
53
57
|
* zero; a re-pin to a CHANGED natural while an enter's transition
|
|
54
58
|
* classes own the frame lands without a height animation, i.e. snaps
|
|
55
59
|
* — acceptable, the enter's own choreography owns that moment). */
|
|
56
60
|
hold(): void;
|
|
61
|
+
/** Drop the height pin and hand the frame back to `height: auto`
|
|
62
|
+
* without disarming anything (a hold()ed morph stays disarmed; an
|
|
63
|
+
* armed one keeps observing). In-flight reveals are torn down first,
|
|
64
|
+
* exactly like stop(). This is the reopen-interrupt edge's tool: a
|
|
65
|
+
* reopen enter must not inherit the previous cycle's pin, or new
|
|
66
|
+
* content sits clipped behind the old height for the whole enter. */
|
|
67
|
+
release(): void;
|
|
57
68
|
/** Re-measure and pin now (resize events, open flows). */
|
|
58
69
|
remeasure(): void;
|
|
59
70
|
/** Plain CSS height morph (round 18): transition the frame's pin
|
|
@@ -69,13 +80,14 @@ export interface RideEntry {
|
|
|
69
80
|
|
|
70
81
|
export interface SizeMorphOptions {
|
|
71
82
|
/** While this returns true, resize-driven re-measurements are deferred
|
|
72
|
-
* (the current pin stays). HkModal freezes the frame's height
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
83
|
+
* (the current pin stays). HkModal freezes the frame's height while a
|
|
84
|
+
* stepflow SLIDE is in flight: the sheet must not chase the flow's
|
|
85
|
+
* changing cell mid-slide, and the slide's morph event flushes the
|
|
86
|
+
* frozen change with an explicit remeasure once the content settles.
|
|
87
|
+
* (The gate used to also cover the modal's own enter — that open
|
|
88
|
+
* choreography is opacity+transform today and never reads the frame's
|
|
89
|
+
* height, so the enter arms nothing and defers nothing; see the morph
|
|
90
|
+
* note in HkModal.) */
|
|
79
91
|
deferRemeasure?: () => boolean;
|
|
80
92
|
/** Fold riders (2026-09-23 round-14 chest report): elements collected
|
|
81
93
|
* when a clip sweep is STAGED whose translateY must track the sweep's
|
|
@@ -623,13 +635,12 @@ export function useSizeMorph(
|
|
|
623
635
|
* finished) frame — the only moment guaranteed free of transition-class
|
|
624
636
|
* flex rules. Never calibrate from a remeasure sample: the first
|
|
625
637
|
* remeasure can itself be the contaminated one (a frozen enter leaves
|
|
626
|
-
* the flex rules behind when the surface is mid-repair).
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
* rather than a corrupted one. */
|
|
638
|
+
* the flex rules behind when the surface is mid-repair). Every current
|
|
639
|
+
* consumer arms at the machine's `open` edge, on a resting frame. If a
|
|
640
|
+
* future caller ever arms earlier (mid-enter), this precondition is
|
|
641
|
+
* violated by construction and the contamination guard below degrades
|
|
642
|
+
* the bad sample to a dropped pin rather than a corrupted one — it
|
|
643
|
+
* does not make early arming correct. */
|
|
633
644
|
function calibrate(): void {
|
|
634
645
|
const f = frame.value;
|
|
635
646
|
const c = content.value;
|
|
@@ -834,9 +845,9 @@ export function useSizeMorph(
|
|
|
834
845
|
}
|
|
835
846
|
|
|
836
847
|
function onResize(): void {
|
|
837
|
-
// Frozen window (
|
|
838
|
-
// the
|
|
839
|
-
//
|
|
848
|
+
// Frozen window (HkModal's stepflow SLIDE freeze — the only defer
|
|
849
|
+
// gate left): keep the current pin; the morph event flushes the
|
|
850
|
+
// accumulated change with an explicit remeasure.
|
|
840
851
|
if (options.deferRemeasure?.()) return;
|
|
841
852
|
// Debounce the choreography: content can change in a burst (a list
|
|
842
853
|
// transition shrinking rows over several frames, a textarea growing
|
|
@@ -1076,5 +1087,5 @@ export function useSizeMorph(
|
|
|
1076
1087
|
setTimeout(restore, durationMs + 350);
|
|
1077
1088
|
}
|
|
1078
1089
|
|
|
1079
|
-
return { start, stop, hold, remeasure, heightMorph };
|
|
1090
|
+
return { start, stop, hold, release, remeasure, heightMorph };
|
|
1080
1091
|
}
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
// HkErrorLanding, so it ships the component's real stylesheet instead of a
|
|
12
12
|
// hand-copied palette. bootLandingCss.test.ts recompiles the entry and fails
|
|
13
13
|
// on drift.
|
|
14
|
-
export const BOOT_LANDING_CSS = ":root{--c-primary-faint: rgb(var(--color-primary, 122 162 247) / 5%);--c-primary-subtle: rgb(var(--color-primary, 122 162 247) / 8%);--c-primary-dim: rgb(var(--color-primary, 122 162 247) / 10%);--c-primary-light: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-overlay: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-medium: rgb(var(--color-primary, 122 162 247) / 25%);--c-primary-strong: rgb(var(--color-primary, 122 162 247) / 40%);--c-primary-intense: rgb(var(--color-primary, 122 162 247) / 40%);--c-error-dim: rgb(var(--color-error, 247 118 142) / 10%);--c-error-overlay: rgb(var(--color-error, 247 118 142) / 20%);--c-error-strong: rgb(var(--color-error, 247 118 142) / 50%);--shadow-elevated: 0 8px 32px rgb(0 0 0 / 15%);--shadow-dropdown: 0 4px 24px rgb(0 0 0 / 12%), 0 0 0 1px rgb(255 255 255 / 4%);--shadow-button: 0 4px 14px rgb(var(--color-primary, 122 162 247) / 35%);--shadow-button-danger: 0 4px 14px rgb(var(--color-error, 247 118 142) / 35%);--shadow-focus: 0 0 0 3px rgb(var(--color-primary, 122 162 247) / 12%);--shadow-focus-error: 0 0 0 3px rgb(var(--color-error, 247 118 142) / 12%);--border-input: rgb(var(--color-border, 100 100 100) / 15%);--border-faint: rgb(var(--color-border, 100 100 100) / 10%);--border-subtle: rgb(var(--color-border, 100 100 100) / 12%);--hi-icon-color: rgb(var(--color-text, 30 30 30));--hi-icon-size-xs: 0.75rem;--hi-icon-size-sm: 1rem;--hi-icon-size-md: 1.25rem;--hi-icon-size-lg: 1.5rem;--hi-duration-instant: var(--duration-instant, 0.1s);--hi-border-color-focus: rgb(var(--color-primary, 122 162 247));--hi-glow-color: transparent;--hi-bg-surface-dark: rgb(var(--color-muted, 108 108 108) / 12%);--hi-color-primary-dark: color-mix(in srgb, rgb(var(--color-primary, 122 162 247)) 82%, #000);--hi-color-secondary-dark: color-mix(in srgb, rgb(var(--color-secondary, 81 154 115)) 82%, #000);--hi-color-danger-dark: color-mix(in srgb, rgb(var(--color-error, 247 118 142)) 82%, #000);--hi-color-success-dark: color-mix(in srgb, rgb(var(--color-success, 14 184 64)) 82%, #000);--hi-color-text-on-secondary: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-danger: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-success: rgb(var(--color-on-solid, 255 255 255))}:root{--color-primary: 122 162 247;--color-on-primary: 255 255 255;--color-on-solid: 255 255 255;--color-on-solid-text: 255 255 255;--color-on-solid-icon: 255 255 255;--color-secondary: 81 154 115;--color-accent: 247 181 0;--color-text: 30 30 30;--color-text-secondary: 102 102 102;--color-text-tertiary: 140 140 140;--color-muted: 108 108 108;--color-border: 100 100 100;--color-focused-border: 122 162 247;--color-background: 255 255 255;--color-surface: 255 255 255;--color-selected-bg: 191 218 242;--color-selected-text: 30 30 30;--color-status-bar-bg: 240 244 248;--color-success: 14 184 64;--color-error: 247 118 142;--color-warning: 238 214 119;--color-info: 106 190 214;--hi-color-primary: rgb(var(--color-primary));--hi-color-secondary: rgb(var(--color-secondary));--hi-color-surface: rgb(var(--color-surface));--hi-color-background: rgb(var(--color-background));--hi-color-border: rgb(var(--color-border) / 15%);--hi-color-text-primary: rgb(var(--color-text));--hi-color-text-secondary: rgb(var(--color-text-secondary));--hi-color-muted: rgb(var(--color-muted));--hi-color-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-solid: rgb(var(--color-on-solid-text, 255 255 255));--hi-color-icon-on-solid: rgb(var(--color-on-solid-icon, 255 255 255));--hi-color-success: rgb(var(--color-success));--hi-color-error: rgb(var(--color-error));--hi-color-warning: rgb(var(--color-warning));--hi-color-info: rgb(var(--color-info));--hi-color-bg-subtle: rgb(var(--color-surface) / 0.5);--hi-color-bg-elevated: rgb(var(--color-surface));--hi-color-bg-canvas: rgb(var(--color-background));--hi-secondary-bg: rgb(var(--color-surface) / 0.5);--hi-color-accent: rgb(var(--color-accent));--hi-primary: var(--hi-color-primary);--hi-surface: var(--hi-color-surface);--hi-card-bg: var(--hi-color-surface)}:root{--space-1: 0.0625rem;--space-2: 0.125rem;--space-4: 0.25rem;--space-6: 0.375rem;--space-8: 0.5rem;--space-10: 0.625rem;--space-12: 0.75rem;--space-14: 0.875rem;--space-16: 1rem;--space-20: 1.25rem;--space-24: 1.5rem;--space-28: 1.75rem;--space-32: 2rem;--space-40: 2.5rem;--text-3xs: 0.5rem;--text-2xs: 0.625rem;--text-xs: 0.75rem;--text-sm: 0.8125rem;--text-base: 0.875rem;--text-md: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--radius-xl: 16px;--radius-full: 9999px;--blur-xs: 4px;--blur-sm: 8px;--blur-md: 16px;--blur-lg: 24px;--blur-xl: 24px;--opacity-half: 0.92;--duration-instant: 0.1s;--duration-short: 0.15s;--duration-fast: 0.15s;--duration-normal: 0.3s;--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);--ease-in: cubic-bezier(0.4, 0, 1, 1);--ease-elastic: cubic-bezier(0.16, 1, 0.3, 1);--hi-duration-fast: 0.15s;--hi-ease-default: cubic-bezier(0.4, 0, 0.2, 1);--hk-z-base: 0;--hk-z-above: 1;--hk-z-scrollbar: 10;--hk-z-fab: 40;--hk-z-toolbar: 50;--hk-z-header: 100;--hk-z-sidebar: 900;--hk-z-drag: 950;--hk-z-breadcrumb: 2500;--hk-z-float: 3000;--hk-z-toast: 4000;--hk-z-veil: 4100;--hk-z-modal: 5000;--hk-z-tooltip: 6000;--hk-z-overlay-root: 90000;--hk-z-hud: 90050;--hk-z-error: 95000;--hk-z-titlebar: 100001;--viewport-gutter: 16px;--font-sans: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif;--font-reading: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif;--font-mono: ui-monospace, \"SF Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", \"Liberation Mono\", \"PingFang SC\", \"Microsoft YaHei\", monospace}@media(max-width: 767px){:root{--viewport-gutter: 8px}}.hk-error-landing{--hel-background: var(--color-background, 246 247 249);--hel-surface: var(--color-surface, 255 255 255);--hel-text: var(--color-text, 30 32 36);--hel-muted: var(--color-muted, 118 122 128);--hel-border: var(--color-border, 222 224 228);--hel-primary: var(--color-primary, 58 118 236);--hel-error: var(--color-error, 226 84 84);--hel-warning: var(--color-warning, 222 168 62);width:100%;min-height:100vh;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:var(--space-20, 1.25rem);background:rgb(var(--hel-background));color:rgb(var(--hel-text));font-family:var(--font-sans, system-ui, -apple-system, \"Segoe UI\", sans-serif)}.hk-error-landing__card{position:relative;overflow:hidden;width:min(440px,100%);background:rgb(var(--hel-surface));border:1px solid rgb(var(--hel-border)/70%);border-radius:var(--radius-xl, 20px);box-shadow:var(--shadow-lg, 0 18px 48px rgba(0, 0, 0, 0.08));padding:2.25rem var(--space-28, 1.75rem) var(--space-28, 1.75rem);text-align:center}.hk-error-landing__card::before{content:\"\";position:absolute;inset:0 0 auto;height:150px;background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-error)/8%), transparent 72%);pointer-events:none}.hk-error-landing.is-warning .hk-error-landing__card::before{background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-warning)/10%), transparent 72%)}.hk-error-landing.is-info .hk-error-landing__card::before{background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-primary)/7%), transparent 72%)}.hk-error-landing__brand{position:relative;display:flex;justify-content:center;margin-bottom:var(--space-16, 1rem)}.hk-error-landing__brand:empty{display:none}.hk-error-landing__icon{position:relative;width:64px;height:64px;margin:0 auto var(--space-14, 0.875rem);display:flex;align-items:center;justify-content:center;border-radius:50%}.hk-error-landing__icon,.hk-error-landing__icon svg{color:rgb(var(--hel-error))}.hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-error)/18%), rgb(var(--hel-error)/6%));border:1px solid rgb(var(--hel-error)/26%);box-shadow:0 0 0 8px rgb(var(--hel-error)/5%),0 10px 28px -12px rgb(var(--hel-error)/45%)}.hk-error-landing.is-warning .hk-error-landing__icon,.hk-error-landing.is-warning .hk-error-landing__icon svg{color:rgb(var(--hel-warning))}.hk-error-landing.is-warning .hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-warning)/20%), rgb(var(--hel-warning)/7%));border-color:rgb(var(--hel-warning)/28%);box-shadow:0 0 0 8px rgb(var(--hel-warning)/6%),0 10px 28px -12px rgb(var(--hel-warning)/45%)}.hk-error-landing.is-info .hk-error-landing__icon,.hk-error-landing.is-info .hk-error-landing__icon svg{color:rgb(var(--hel-primary))}.hk-error-landing.is-info .hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-primary)/16%), rgb(var(--hel-primary)/5%));border-color:rgb(var(--hel-primary)/24%);box-shadow:0 0 0 8px rgb(var(--hel-primary)/5%),0 10px 28px -12px rgb(var(--hel-primary)/40%)}.hk-error-landing__meta{position:relative;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--space-6, 0.375rem);margin-bottom:var(--space-8, 0.5rem)}.hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-error));--hk-badge-bg: rgb(var(--hel-error) / 10%);--hk-badge-border: rgb(var(--hel-error) / 22%)}.hk-error-landing.is-warning .hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-warning));--hk-badge-bg: rgb(var(--hel-warning) / 12%);--hk-badge-border: rgb(var(--hel-warning) / 24%)}.hk-error-landing.is-info .hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-primary));--hk-badge-bg: rgb(var(--hel-primary) / 10%);--hk-badge-border: rgb(var(--hel-primary) / 20%)}.hk-error-landing__meta .hk-badge-muted{--hk-badge-text: rgb(var(--hel-muted));--hk-badge-bg: rgb(var(--hel-muted) / 8%);--hk-badge-border: rgb(var(--hel-border) / 80%)}.hk-error-landing__title{position:relative;margin:0;font-size:var(--text-xl, 1.25rem);font-weight:650;letter-spacing:-0.01em;line-height:1.35;color:rgb(var(--hel-text));white-space:pre-line;overflow-wrap:anywhere}.hk-error-landing__desc{position:relative;margin:var(--space-8, 0.5rem) 0 0;font-size:var(--text-sm, 0.8125rem);line-height:1.6;color:rgb(var(--hel-muted));white-space:pre-line;overflow-wrap:anywhere}.hk-error-landing__details{position:relative;margin-top:var(--space-16, 1rem);text-align:start}.hk-error-landing__details-label{display:flex;align-items:center;gap:var(--space-4, 4px);padding:0 var(--space-2, 2px);font-size:var(--text-xs, 0.75rem);font-weight:500;letter-spacing:.02em;color:rgb(var(--hel-muted));-webkit-user-select:none;user-select:none}.hk-error-landing__details-pane{position:relative;margin-top:var(--space-6, 0.375rem);height:max(9rem,20vh);height:max(9rem,20dvh);border:1px solid rgb(var(--hel-border)/70%);border-radius:var(--radius-md, 10px);background:rgb(var(--hel-muted)/4%)}.hk-error-landing__details-body{height:100%;overflow:auto;border-radius:inherit;padding:var(--space-4, 0.25rem);scrollbar-width:none}.hk-error-landing__details-body::-webkit-scrollbar{display:none}.hk-error-landing .hk-error-landing__details-body .s-tool-json-tree{max-height:none;height:auto;overflow:visible;margin:0;padding:0;border:none;border-radius:0;background:rgba(0,0,0,0)}.hk-error-landing__actions{position:relative;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--space-8, 0.5rem);margin-top:var(--space-16, 1rem)}.hk-error-landing.is-inline{min-height:100%;padding:0 var(--space-12, 0.75rem);align-items:flex-start;background:rgba(0,0,0,0)}.hk-error-landing.is-inline.is-fill{flex:1 1 auto;min-height:0}.hk-error-landing.is-inline .hk-error-landing__card{margin:auto;padding:var(--space-16, 1rem) var(--space-28, 1.75rem) var(--space-24, 1.5rem);display:flex;flex-direction:column;max-height:100%;min-height:min-content}.hk-error-landing.is-inline .hk-error-landing__details{display:flex;flex-direction:column;min-height:0}.hk-error-landing.is-inline .hk-error-landing__details-pane{height:auto;flex-grow:0;flex-shrink:1;flex-basis:max(9rem,20vh);flex-basis:max(9rem,20dvh);min-height:6rem}@media(max-width: 480px){.hk-error-landing__card{padding:var(--space-28, 1.75rem) 1.125rem var(--space-24, 1.5rem)}}:root{--c-primary-faint: rgb(var(--color-primary, 122 162 247) / 5%);--c-primary-subtle: rgb(var(--color-primary, 122 162 247) / 8%);--c-primary-dim: rgb(var(--color-primary, 122 162 247) / 10%);--c-primary-light: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-overlay: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-medium: rgb(var(--color-primary, 122 162 247) / 25%);--c-primary-strong: rgb(var(--color-primary, 122 162 247) / 40%);--c-primary-intense: rgb(var(--color-primary, 122 162 247) / 40%);--c-error-dim: rgb(var(--color-error, 247 118 142) / 10%);--c-error-overlay: rgb(var(--color-error, 247 118 142) / 20%);--c-error-strong: rgb(var(--color-error, 247 118 142) / 50%);--shadow-elevated: 0 8px 32px rgb(0 0 0 / 15%);--shadow-dropdown: 0 4px 24px rgb(0 0 0 / 12%), 0 0 0 1px rgb(255 255 255 / 4%);--shadow-button: 0 4px 14px rgb(var(--color-primary, 122 162 247) / 35%);--shadow-button-danger: 0 4px 14px rgb(var(--color-error, 247 118 142) / 35%);--shadow-focus: 0 0 0 3px rgb(var(--color-primary, 122 162 247) / 12%);--shadow-focus-error: 0 0 0 3px rgb(var(--color-error, 247 118 142) / 12%);--border-input: rgb(var(--color-border, 100 100 100) / 15%);--border-faint: rgb(var(--color-border, 100 100 100) / 10%);--border-subtle: rgb(var(--color-border, 100 100 100) / 12%);--hi-icon-color: rgb(var(--color-text, 30 30 30));--hi-icon-size-xs: 0.75rem;--hi-icon-size-sm: 1rem;--hi-icon-size-md: 1.25rem;--hi-icon-size-lg: 1.5rem;--hi-duration-instant: var(--duration-instant, 0.1s);--hi-border-color-focus: rgb(var(--color-primary, 122 162 247));--hi-glow-color: transparent;--hi-bg-surface-dark: rgb(var(--color-muted, 108 108 108) / 12%);--hi-color-primary-dark: color-mix(in srgb, rgb(var(--color-primary, 122 162 247)) 82%, #000);--hi-color-secondary-dark: color-mix(in srgb, rgb(var(--color-secondary, 81 154 115)) 82%, #000);--hi-color-danger-dark: color-mix(in srgb, rgb(var(--color-error, 247 118 142)) 82%, #000);--hi-color-success-dark: color-mix(in srgb, rgb(var(--color-success, 14 184 64)) 82%, #000);--hi-color-text-on-secondary: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-danger: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-success: rgb(var(--color-on-solid, 255 255 255))}:root{--color-primary: 122 162 247;--color-on-primary: 255 255 255;--color-on-solid: 255 255 255;--color-on-solid-text: 255 255 255;--color-on-solid-icon: 255 255 255;--color-secondary: 81 154 115;--color-accent: 247 181 0;--color-text: 30 30 30;--color-text-secondary: 102 102 102;--color-text-tertiary: 140 140 140;--color-muted: 108 108 108;--color-border: 100 100 100;--color-focused-border: 122 162 247;--color-background: 255 255 255;--color-surface: 255 255 255;--color-selected-bg: 191 218 242;--color-selected-text: 30 30 30;--color-status-bar-bg: 240 244 248;--color-success: 14 184 64;--color-error: 247 118 142;--color-warning: 238 214 119;--color-info: 106 190 214;--hi-color-primary: rgb(var(--color-primary));--hi-color-secondary: rgb(var(--color-secondary));--hi-color-surface: rgb(var(--color-surface));--hi-color-background: rgb(var(--color-background));--hi-color-border: rgb(var(--color-border) / 15%);--hi-color-text-primary: rgb(var(--color-text));--hi-color-text-secondary: rgb(var(--color-text-secondary));--hi-color-muted: rgb(var(--color-muted));--hi-color-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-solid: rgb(var(--color-on-solid-text, 255 255 255));--hi-color-icon-on-solid: rgb(var(--color-on-solid-icon, 255 255 255));--hi-color-success: rgb(var(--color-success));--hi-color-error: rgb(var(--color-error));--hi-color-warning: rgb(var(--color-warning));--hi-color-info: rgb(var(--color-info));--hi-color-bg-subtle: rgb(var(--color-surface) / 0.5);--hi-color-bg-elevated: rgb(var(--color-surface));--hi-color-bg-canvas: rgb(var(--color-background));--hi-secondary-bg: rgb(var(--color-surface) / 0.5);--hi-color-accent: rgb(var(--color-accent));--hi-primary: var(--hi-color-primary);--hi-surface: var(--hi-color-surface);--hi-card-bg: var(--hi-color-surface)}.hk-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-6);font-family:inherit;font-weight:600;line-height:1;border:1px solid rgba(0,0,0,0);border-radius:var(--radius-sm);cursor:pointer;transition-property:background-color,border-color,color,box-shadow,opacity,transform,filter;transition-duration:var(--duration-normal);transition-timing-function:var(--ease-standard);white-space:nowrap;-webkit-user-select:none;user-select:none;outline:none;position:relative}.hk-btn:focus-visible{box-shadow:0 0 0 2px rgb(var(--color-surface)),0 0 0 4px rgb(var(--color-primary))}.hk-btn:disabled{opacity:.5;cursor:not-allowed}.hk-btn-xs{padding:var(--space-4) var(--space-8);font-size:var(--text-xs);min-height:1.375rem}.hk-btn-sm{padding:var(--space-6) var(--space-12);font-size:var(--text-xs);min-height:1.75rem}.hk-btn-md{padding:var(--space-8) var(--space-16);font-size:var(--text-base);min-height:2.5rem}.hk-btn-lg{padding:var(--space-10) var(--space-24);font-size:var(--text-base);min-height:2.75rem}.hk-btn-xs .hk-btn-icon,.hk-btn-xs .hk-btn-suffix{inline-size:.8125rem;block-size:.8125rem}.hk-btn-xs .hk-btn-icon .hk-icon,.hk-btn-xs .hk-btn-suffix .hk-icon{inline-size:100%;block-size:100%}.hk-btn-icon,.hk-btn-suffix{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:1rem;block-size:1rem;line-height:0}.hk-btn-icon svg,.hk-btn-suffix svg{display:block}.hk-btn>svg{flex:none}.hk-btn-icon-only{padding:0;box-sizing:border-box}.hk-btn-xs.hk-btn-icon-only{width:1.375rem}.hk-btn-sm.hk-btn-icon-only{width:1.75rem}.hk-btn-md.hk-btn-icon-only{width:2.5rem}.hk-btn-lg.hk-btn-icon-only{width:2.75rem}.hk-btn-block.hk-btn-icon-only{width:100%}.hk-btn-label{display:inline-flex;align-items:center;min-width:0}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}.hk-btn-primary{background:rgb(var(--color-primary));color:rgb(var(--color-on-solid))}.hk-btn-primary svg{color:rgb(var(--color-on-solid-icon, 255 255 255))}.hk-btn-primary:hover:not(:disabled):not(:focus-visible){filter:brightness(1.1);box-shadow:var(--shadow-button)}.hk-btn-primary:active:not(:disabled){filter:brightness(0.95)}.hk-btn-secondary{background:rgb(var(--color-surface));color:rgb(var(--color-text));border-color:rgb(var(--color-border)/20%);-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}.hk-btn-secondary:hover:not(:disabled){border-color:var(--c-primary-strong)}.hk-btn-secondary:active:not(:disabled){filter:brightness(0.95)}.hk-btn-outline{background:rgba(0,0,0,0);color:rgb(var(--color-primary));border-color:var(--c-primary-intense)}.hk-btn-outline:hover:not(:disabled){background:var(--c-primary-light);border-color:rgb(var(--color-primary))}.hk-btn-outline:active:not(:disabled){filter:brightness(0.95)}.hk-btn-ghost{background:rgba(0,0,0,0);color:rgb(var(--color-text));border-color:rgba(0,0,0,0)}.hk-btn-ghost:hover:not(:disabled){background:var(--c-primary-light)}.hk-btn-ghost:active:not(:disabled){filter:brightness(0.95)}.hk-btn-danger{background:rgb(var(--color-error));color:rgb(var(--color-on-solid))}.hk-btn-danger:hover:not(:disabled):not(:focus-visible){filter:brightness(1.1);box-shadow:var(--shadow-button-danger)}.hk-btn-danger:active:not(:disabled){filter:brightness(0.95)}.hk-btn-block{width:100%}.hk-btn-loading{pointer-events:none}@media(prefers-color-scheme: dark){:root{--color-primary: 136 192 208;--color-on-solid: 46 52 64;--color-on-solid-text: 46 52 64;--color-on-solid-icon: 46 52 64;--color-text: 247 249 252;--color-muted: 180 180 180;--color-border: 128 128 128;--color-background: 22 27 38;--color-surface: 34 40 54;--color-error: 191 97 106}}";
|
|
14
|
+
export const BOOT_LANDING_CSS = ":root{--c-primary-faint: rgb(var(--color-primary, 122 162 247) / 5%);--c-primary-subtle: rgb(var(--color-primary, 122 162 247) / 8%);--c-primary-dim: rgb(var(--color-primary, 122 162 247) / 10%);--c-primary-light: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-overlay: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-medium: rgb(var(--color-primary, 122 162 247) / 25%);--c-primary-strong: rgb(var(--color-primary, 122 162 247) / 40%);--c-primary-intense: rgb(var(--color-primary, 122 162 247) / 40%);--c-error-dim: rgb(var(--color-error, 247 118 142) / 10%);--c-error-overlay: rgb(var(--color-error, 247 118 142) / 20%);--c-error-strong: rgb(var(--color-error, 247 118 142) / 50%);--shadow-elevated: 0 8px 32px rgb(0 0 0 / 15%);--shadow-dropdown: 0 4px 24px rgb(0 0 0 / 12%), 0 0 0 1px rgb(255 255 255 / 4%);--shadow-button: 0 4px 14px rgb(var(--color-primary, 122 162 247) / 35%);--shadow-button-danger: 0 4px 14px rgb(var(--color-error, 247 118 142) / 35%);--shadow-focus: 0 0 0 3px rgb(var(--color-primary, 122 162 247) / 12%);--shadow-focus-error: 0 0 0 3px rgb(var(--color-error, 247 118 142) / 12%);--border-input: rgb(var(--color-border, 100 100 100) / 15%);--border-faint: rgb(var(--color-border, 100 100 100) / 10%);--border-subtle: rgb(var(--color-border, 100 100 100) / 12%);--hi-icon-color: rgb(var(--color-text, 30 30 30));--hi-icon-size-xs: 0.75rem;--hi-icon-size-sm: 1rem;--hi-icon-size-md: 1.25rem;--hi-icon-size-lg: 1.5rem;--hi-duration-instant: var(--duration-instant, 0.1s);--hi-border-color-focus: rgb(var(--color-primary, 122 162 247));--hi-glow-color: transparent;--hi-bg-surface-dark: rgb(var(--color-muted, 108 108 108) / 12%);--hi-color-primary-dark: color-mix(in srgb, rgb(var(--color-primary, 122 162 247)) 82%, #000);--hi-color-secondary-dark: color-mix(in srgb, rgb(var(--color-secondary, 81 154 115)) 82%, #000);--hi-color-danger-dark: color-mix(in srgb, rgb(var(--color-error, 247 118 142)) 82%, #000);--hi-color-success-dark: color-mix(in srgb, rgb(var(--color-success, 14 184 64)) 82%, #000);--hi-color-text-on-secondary: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-danger: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-success: rgb(var(--color-on-solid, 255 255 255))}:root{--color-primary: 122 162 247;--color-on-primary: 255 255 255;--color-on-solid: 255 255 255;--color-on-solid-text: 255 255 255;--color-on-solid-icon: 255 255 255;--color-secondary: 81 154 115;--color-accent: 247 181 0;--color-text: 30 30 30;--color-text-secondary: 102 102 102;--color-text-tertiary: 140 140 140;--color-muted: 108 108 108;--color-border: 100 100 100;--color-focused-border: 122 162 247;--color-background: 255 255 255;--color-surface: 255 255 255;--color-selected-bg: 191 218 242;--color-selected-text: 30 30 30;--color-status-bar-bg: 240 244 248;--color-success: 14 184 64;--color-error: 247 118 142;--color-warning: 238 214 119;--color-info: 106 190 214;--hi-color-primary: rgb(var(--color-primary));--hi-color-secondary: rgb(var(--color-secondary));--hi-color-surface: rgb(var(--color-surface));--hi-color-background: rgb(var(--color-background));--hi-color-border: rgb(var(--color-border) / 15%);--hi-color-text-primary: rgb(var(--color-text));--hi-color-text-secondary: rgb(var(--color-text-secondary));--hi-color-muted: rgb(var(--color-muted));--hi-color-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-solid: rgb(var(--color-on-solid-text, 255 255 255));--hi-color-icon-on-solid: rgb(var(--color-on-solid-icon, 255 255 255));--hi-color-success: rgb(var(--color-success));--hi-color-error: rgb(var(--color-error));--hi-color-warning: rgb(var(--color-warning));--hi-color-info: rgb(var(--color-info));--hi-color-bg-subtle: rgb(var(--color-surface) / 0.5);--hi-color-bg-elevated: rgb(var(--color-surface));--hi-color-bg-canvas: rgb(var(--color-background));--hi-secondary-bg: rgb(var(--color-surface) / 0.5);--hi-color-accent: rgb(var(--color-accent));--hi-primary: var(--hi-color-primary);--hi-surface: var(--hi-color-surface);--hi-card-bg: var(--hi-color-surface)}:root{--space-1: 0.0625rem;--space-2: 0.125rem;--space-4: 0.25rem;--space-6: 0.375rem;--space-8: 0.5rem;--space-10: 0.625rem;--space-12: 0.75rem;--space-14: 0.875rem;--space-16: 1rem;--space-20: 1.25rem;--space-24: 1.5rem;--space-28: 1.75rem;--space-32: 2rem;--space-40: 2.5rem;--text-3xs: 0.5rem;--text-2xs: 0.625rem;--text-xs: 0.75rem;--text-sm: 0.8125rem;--text-base: 0.875rem;--text-md: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--radius-xl: 16px;--radius-full: 9999px;--blur-xs: 4px;--blur-sm: 8px;--blur-md: 16px;--blur-lg: 24px;--blur-xl: 24px;--opacity-half: 0.92;--duration-instant: 0.1s;--duration-short: 0.15s;--duration-fast: 0.15s;--duration-normal: 0.3s;--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);--ease-in: cubic-bezier(0.4, 0, 1, 1);--ease-elastic: cubic-bezier(0.16, 1, 0.3, 1);--hi-duration-fast: 0.15s;--hi-ease-default: cubic-bezier(0.4, 0, 0.2, 1);--hk-z-base: 0;--hk-z-above: 1;--hk-z-scrollbar: 10;--hk-z-fab: 40;--hk-z-toolbar: 50;--hk-z-header: 100;--hk-z-sidebar: 900;--hk-z-drag: 950;--hk-z-breadcrumb: 2500;--hk-z-float: 3000;--hk-z-toast: 4000;--hk-z-veil: 4100;--hk-z-modal: 5000;--hk-z-tooltip: 6000;--hk-z-overlay-root: 90000;--hk-z-hud: 90050;--hk-z-error: 95000;--hk-z-titlebar: 100001;--viewport-gutter: 16px;--font-sans: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif;--font-reading: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif;--font-mono: ui-monospace, \"SF Mono\", Menlo, Consolas, \"DejaVu Sans Mono\", \"Liberation Mono\", \"PingFang SC\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", monospace}@media(max-width: 767px){:root{--viewport-gutter: 8px}}.hk-error-landing{--hel-background: var(--color-background, 246 247 249);--hel-surface: var(--color-surface, 255 255 255);--hel-text: var(--color-text, 30 32 36);--hel-muted: var(--color-muted, 118 122 128);--hel-border: var(--color-border, 222 224 228);--hel-primary: var(--color-primary, 58 118 236);--hel-error: var(--color-error, 226 84 84);--hel-warning: var(--color-warning, 222 168 62);width:100%;min-height:100vh;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:var(--space-20, 1.25rem);background:rgb(var(--hel-background));color:rgb(var(--hel-text));font-family:var(--font-sans, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", \"Noto Sans CJK SC\", sans-serif)}.hk-error-landing__card{position:relative;overflow:hidden;width:min(440px,100%);background:rgb(var(--hel-surface));border:1px solid rgb(var(--hel-border)/70%);border-radius:var(--radius-xl, 20px);box-shadow:var(--shadow-lg, 0 18px 48px rgba(0, 0, 0, 0.08));padding:2.25rem var(--space-28, 1.75rem) var(--space-28, 1.75rem);text-align:center}.hk-error-landing__card::before{content:\"\";position:absolute;inset:0 0 auto;height:150px;background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-error)/8%), transparent 72%);pointer-events:none}.hk-error-landing.is-warning .hk-error-landing__card::before{background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-warning)/10%), transparent 72%)}.hk-error-landing.is-info .hk-error-landing__card::before{background:radial-gradient(58% 100% at 50% 0%, rgb(var(--hel-primary)/7%), transparent 72%)}.hk-error-landing__brand{position:relative;display:flex;justify-content:center;margin-bottom:var(--space-16, 1rem)}.hk-error-landing__brand:empty{display:none}.hk-error-landing__icon{position:relative;width:64px;height:64px;margin:0 auto var(--space-14, 0.875rem);display:flex;align-items:center;justify-content:center;border-radius:50%}.hk-error-landing__icon,.hk-error-landing__icon svg{color:rgb(var(--hel-error))}.hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-error)/18%), rgb(var(--hel-error)/6%));border:1px solid rgb(var(--hel-error)/26%);box-shadow:0 0 0 8px rgb(var(--hel-error)/5%),0 10px 28px -12px rgb(var(--hel-error)/45%)}.hk-error-landing.is-warning .hk-error-landing__icon,.hk-error-landing.is-warning .hk-error-landing__icon svg{color:rgb(var(--hel-warning))}.hk-error-landing.is-warning .hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-warning)/20%), rgb(var(--hel-warning)/7%));border-color:rgb(var(--hel-warning)/28%);box-shadow:0 0 0 8px rgb(var(--hel-warning)/6%),0 10px 28px -12px rgb(var(--hel-warning)/45%)}.hk-error-landing.is-info .hk-error-landing__icon,.hk-error-landing.is-info .hk-error-landing__icon svg{color:rgb(var(--hel-primary))}.hk-error-landing.is-info .hk-error-landing__icon{background:linear-gradient(145deg, rgb(var(--hel-primary)/16%), rgb(var(--hel-primary)/5%));border-color:rgb(var(--hel-primary)/24%);box-shadow:0 0 0 8px rgb(var(--hel-primary)/5%),0 10px 28px -12px rgb(var(--hel-primary)/40%)}.hk-error-landing__meta{position:relative;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--space-6, 0.375rem);margin-bottom:var(--space-8, 0.5rem)}.hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-error));--hk-badge-bg: rgb(var(--hel-error) / 10%);--hk-badge-border: rgb(var(--hel-error) / 22%)}.hk-error-landing.is-warning .hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-warning));--hk-badge-bg: rgb(var(--hel-warning) / 12%);--hk-badge-border: rgb(var(--hel-warning) / 24%)}.hk-error-landing.is-info .hk-error-landing__meta .hk-badge:not(.hk-badge-muted){--hk-badge-text: rgb(var(--hel-primary));--hk-badge-bg: rgb(var(--hel-primary) / 10%);--hk-badge-border: rgb(var(--hel-primary) / 20%)}.hk-error-landing__meta .hk-badge-muted{--hk-badge-text: rgb(var(--hel-muted));--hk-badge-bg: rgb(var(--hel-muted) / 8%);--hk-badge-border: rgb(var(--hel-border) / 80%)}.hk-error-landing__title{position:relative;margin:0;font-size:var(--text-xl, 1.25rem);font-weight:650;letter-spacing:-0.01em;line-height:1.35;color:rgb(var(--hel-text));white-space:pre-line;overflow-wrap:anywhere}.hk-error-landing__desc{position:relative;margin:var(--space-8, 0.5rem) 0 0;font-size:var(--text-sm, 0.8125rem);line-height:1.6;color:rgb(var(--hel-muted));white-space:pre-line;overflow-wrap:anywhere}.hk-error-landing__details{position:relative;margin-top:var(--space-16, 1rem);text-align:start}.hk-error-landing__details-label{display:flex;align-items:center;gap:var(--space-4, 4px);padding:0 var(--space-2, 2px);font-size:var(--text-xs, 0.75rem);font-weight:500;letter-spacing:.02em;color:rgb(var(--hel-muted));-webkit-user-select:none;user-select:none}.hk-error-landing__details-pane{position:relative;margin-top:var(--space-6, 0.375rem);height:max(9rem,20vh);height:max(9rem,20dvh);border:1px solid rgb(var(--hel-border)/70%);border-radius:var(--radius-md, 10px);background:rgb(var(--hel-muted)/4%)}.hk-error-landing__details-body{height:100%;overflow:auto;border-radius:inherit;padding:var(--space-4, 0.25rem);scrollbar-width:none}.hk-error-landing__details-body::-webkit-scrollbar{display:none}.hk-error-landing .hk-error-landing__details-body .s-tool-json-tree{max-height:none;height:auto;overflow:visible;margin:0;padding:0;border:none;border-radius:0;background:rgba(0,0,0,0)}.hk-error-landing__actions{position:relative;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:var(--space-8, 0.5rem);margin-top:var(--space-16, 1rem)}.hk-error-landing.is-inline{min-height:100%;padding:0 var(--space-12, 0.75rem);align-items:flex-start;background:rgba(0,0,0,0)}.hk-error-landing.is-inline.is-fill{flex:1 1 auto;min-height:0}.hk-error-landing.is-inline .hk-error-landing__card{margin:auto;padding:var(--space-16, 1rem) var(--space-28, 1.75rem) var(--space-24, 1.5rem);display:flex;flex-direction:column;max-height:100%;min-height:min-content}.hk-error-landing.is-inline .hk-error-landing__details{display:flex;flex-direction:column;min-height:0}.hk-error-landing.is-inline .hk-error-landing__details-pane{height:auto;flex-grow:0;flex-shrink:1;flex-basis:max(9rem,20vh);flex-basis:max(9rem,20dvh);min-height:6rem}@media(max-width: 480px){.hk-error-landing__card{padding:var(--space-28, 1.75rem) 1.125rem var(--space-24, 1.5rem)}}:root{--c-primary-faint: rgb(var(--color-primary, 122 162 247) / 5%);--c-primary-subtle: rgb(var(--color-primary, 122 162 247) / 8%);--c-primary-dim: rgb(var(--color-primary, 122 162 247) / 10%);--c-primary-light: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-overlay: rgb(var(--color-primary, 122 162 247) / 15%);--c-primary-medium: rgb(var(--color-primary, 122 162 247) / 25%);--c-primary-strong: rgb(var(--color-primary, 122 162 247) / 40%);--c-primary-intense: rgb(var(--color-primary, 122 162 247) / 40%);--c-error-dim: rgb(var(--color-error, 247 118 142) / 10%);--c-error-overlay: rgb(var(--color-error, 247 118 142) / 20%);--c-error-strong: rgb(var(--color-error, 247 118 142) / 50%);--shadow-elevated: 0 8px 32px rgb(0 0 0 / 15%);--shadow-dropdown: 0 4px 24px rgb(0 0 0 / 12%), 0 0 0 1px rgb(255 255 255 / 4%);--shadow-button: 0 4px 14px rgb(var(--color-primary, 122 162 247) / 35%);--shadow-button-danger: 0 4px 14px rgb(var(--color-error, 247 118 142) / 35%);--shadow-focus: 0 0 0 3px rgb(var(--color-primary, 122 162 247) / 12%);--shadow-focus-error: 0 0 0 3px rgb(var(--color-error, 247 118 142) / 12%);--border-input: rgb(var(--color-border, 100 100 100) / 15%);--border-faint: rgb(var(--color-border, 100 100 100) / 10%);--border-subtle: rgb(var(--color-border, 100 100 100) / 12%);--hi-icon-color: rgb(var(--color-text, 30 30 30));--hi-icon-size-xs: 0.75rem;--hi-icon-size-sm: 1rem;--hi-icon-size-md: 1.25rem;--hi-icon-size-lg: 1.5rem;--hi-duration-instant: var(--duration-instant, 0.1s);--hi-border-color-focus: rgb(var(--color-primary, 122 162 247));--hi-glow-color: transparent;--hi-bg-surface-dark: rgb(var(--color-muted, 108 108 108) / 12%);--hi-color-primary-dark: color-mix(in srgb, rgb(var(--color-primary, 122 162 247)) 82%, #000);--hi-color-secondary-dark: color-mix(in srgb, rgb(var(--color-secondary, 81 154 115)) 82%, #000);--hi-color-danger-dark: color-mix(in srgb, rgb(var(--color-error, 247 118 142)) 82%, #000);--hi-color-success-dark: color-mix(in srgb, rgb(var(--color-success, 14 184 64)) 82%, #000);--hi-color-text-on-secondary: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-danger: rgb(var(--color-on-solid, 255 255 255));--hi-color-text-on-success: rgb(var(--color-on-solid, 255 255 255))}:root{--color-primary: 122 162 247;--color-on-primary: 255 255 255;--color-on-solid: 255 255 255;--color-on-solid-text: 255 255 255;--color-on-solid-icon: 255 255 255;--color-secondary: 81 154 115;--color-accent: 247 181 0;--color-text: 30 30 30;--color-text-secondary: 102 102 102;--color-text-tertiary: 140 140 140;--color-muted: 108 108 108;--color-border: 100 100 100;--color-focused-border: 122 162 247;--color-background: 255 255 255;--color-surface: 255 255 255;--color-selected-bg: 191 218 242;--color-selected-text: 30 30 30;--color-status-bar-bg: 240 244 248;--color-success: 14 184 64;--color-error: 247 118 142;--color-warning: 238 214 119;--color-info: 106 190 214;--hi-color-primary: rgb(var(--color-primary));--hi-color-secondary: rgb(var(--color-secondary));--hi-color-surface: rgb(var(--color-surface));--hi-color-background: rgb(var(--color-background));--hi-color-border: rgb(var(--color-border) / 15%);--hi-color-text-primary: rgb(var(--color-text));--hi-color-text-secondary: rgb(var(--color-text-secondary));--hi-color-muted: rgb(var(--color-muted));--hi-color-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-primary: rgb(var(--color-on-primary));--hi-color-text-on-solid: rgb(var(--color-on-solid-text, 255 255 255));--hi-color-icon-on-solid: rgb(var(--color-on-solid-icon, 255 255 255));--hi-color-success: rgb(var(--color-success));--hi-color-error: rgb(var(--color-error));--hi-color-warning: rgb(var(--color-warning));--hi-color-info: rgb(var(--color-info));--hi-color-bg-subtle: rgb(var(--color-surface) / 0.5);--hi-color-bg-elevated: rgb(var(--color-surface));--hi-color-bg-canvas: rgb(var(--color-background));--hi-secondary-bg: rgb(var(--color-surface) / 0.5);--hi-color-accent: rgb(var(--color-accent));--hi-primary: var(--hi-color-primary);--hi-surface: var(--hi-color-surface);--hi-card-bg: var(--hi-color-surface)}.hk-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-6);font-family:inherit;font-weight:600;line-height:1;border:1px solid rgba(0,0,0,0);border-radius:var(--radius-sm);cursor:pointer;transition-property:background-color,border-color,color,box-shadow,opacity,transform,filter;transition-duration:var(--duration-normal);transition-timing-function:var(--ease-standard);white-space:nowrap;-webkit-user-select:none;user-select:none;outline:none;position:relative}.hk-btn:focus-visible{box-shadow:0 0 0 2px rgb(var(--color-surface)),0 0 0 4px rgb(var(--color-primary))}.hk-btn:disabled{opacity:.5;cursor:not-allowed}.hk-btn-xs{padding:var(--space-4) var(--space-8);font-size:var(--text-xs);min-height:1.375rem}.hk-btn-sm{padding:var(--space-6) var(--space-12);font-size:var(--text-xs);min-height:1.75rem}.hk-btn-md{padding:var(--space-8) var(--space-16);font-size:var(--text-base);min-height:2.5rem}.hk-btn-lg{padding:var(--space-10) var(--space-24);font-size:var(--text-base);min-height:2.75rem}.hk-btn-xs .hk-btn-icon,.hk-btn-xs .hk-btn-suffix{inline-size:.8125rem;block-size:.8125rem}.hk-btn-xs .hk-btn-icon .hk-icon,.hk-btn-xs .hk-btn-suffix .hk-icon{inline-size:100%;block-size:100%}.hk-btn-icon,.hk-btn-suffix{display:inline-flex;align-items:center;justify-content:center;flex:none;inline-size:1rem;block-size:1rem;line-height:0}.hk-btn-icon svg,.hk-btn-suffix svg{display:block}.hk-btn>svg{flex:none}.hk-btn-icon-only{padding:0;box-sizing:border-box}.hk-btn-xs.hk-btn-icon-only{width:1.375rem}.hk-btn-sm.hk-btn-icon-only{width:1.75rem}.hk-btn-md.hk-btn-icon-only{width:2.5rem}.hk-btn-lg.hk-btn-icon-only{width:2.75rem}.hk-btn-block.hk-btn-icon-only{width:100%}.hk-btn-label{display:inline-flex;align-items:center;min-width:0}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 639.98px){.hk-btn-label-collapse-sm.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-sm.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 767.98px){.hk-btn-label-collapse-md.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-md.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 1023.98px){.hk-btn-label-collapse-lg.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-lg.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-xs .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-xs{padding:0;box-sizing:border-box;width:1.375rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-sm .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-sm{padding:0;box-sizing:border-box;width:1.75rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-md .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-md{padding:0;box-sizing:border-box;width:2.5rem}}@media(max-width: 1279.98px){.hk-btn-label-collapse-xl.hk-btn-lg .hk-btn-label{display:none}.hk-btn-label-collapse-xl.hk-btn-lg{padding:0;box-sizing:border-box;width:2.75rem}}.hk-btn-primary{background:rgb(var(--color-primary));color:rgb(var(--color-on-solid))}.hk-btn-primary svg{color:rgb(var(--color-on-solid-icon, 255 255 255))}.hk-btn-primary:hover:not(:disabled):not(:focus-visible){filter:brightness(1.1);box-shadow:var(--shadow-button)}.hk-btn-primary:active:not(:disabled){filter:brightness(0.95)}.hk-btn-secondary{background:rgb(var(--color-surface));color:rgb(var(--color-text));border-color:rgb(var(--color-border)/20%);-webkit-backdrop-filter:blur(var(--blur-sm));backdrop-filter:blur(var(--blur-sm))}.hk-btn-secondary:hover:not(:disabled){border-color:var(--c-primary-strong)}.hk-btn-secondary:active:not(:disabled){filter:brightness(0.95)}.hk-btn-outline{background:rgba(0,0,0,0);color:rgb(var(--color-primary));border-color:var(--c-primary-intense)}.hk-btn-outline:hover:not(:disabled){background:var(--c-primary-light);border-color:rgb(var(--color-primary))}.hk-btn-outline:active:not(:disabled){filter:brightness(0.95)}.hk-btn-ghost{background:rgba(0,0,0,0);color:rgb(var(--color-text));border-color:rgba(0,0,0,0)}.hk-btn-ghost:hover:not(:disabled){background:var(--c-primary-light)}.hk-btn-ghost:active:not(:disabled){filter:brightness(0.95)}.hk-btn-danger{background:rgb(var(--color-error));color:rgb(var(--color-on-solid))}.hk-btn-danger:hover:not(:disabled):not(:focus-visible){filter:brightness(1.1);box-shadow:var(--shadow-button-danger)}.hk-btn-danger:active:not(:disabled){filter:brightness(0.95)}.hk-btn-block{width:100%}.hk-btn-loading{pointer-events:none}@media(prefers-color-scheme: dark){:root{--color-primary: 136 192 208;--color-on-solid: 46 52 64;--color-on-solid-text: 46 52 64;--color-on-solid-icon: 46 52 64;--color-text: 247 249 252;--color-muted: 180 180 180;--color-border: 128 128 128;--color-background: 22 27 38;--color-surface: 34 40 54;--color-error: 191 97 106}}";
|
|
@@ -27,6 +27,13 @@ import { BOOT_LANDING_CSS } from "./bootLandingCss.mjs";
|
|
|
27
27
|
* "reuse the component" a mechanical property instead of a promise (edit the
|
|
28
28
|
* component sheet and the panel follows through one regeneration).
|
|
29
29
|
*
|
|
30
|
+
* Scope note (2026-11-07 mutation round): the entry @uses the VENDORED
|
|
31
|
+
* `styles/theme/scale.scss`, not the canonical `src/scale.scss` — drifting
|
|
32
|
+
* the canonical copy alone turns fontStacks.contract.test.ts red (three
|
|
33
|
+
* arms) but is invisible here. Token edits: change src/scale.scss, re-copy
|
|
34
|
+
* to the vendored path, regenerate this artifact — all three guards then
|
|
35
|
+
* see the same bytes.
|
|
36
|
+
*
|
|
30
37
|
* Regenerate with (from packages/vue): pnpm run gen:boot-landing-css
|
|
31
38
|
*/
|
|
32
39
|
|
package/src/scale.scss
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
/* Fonts — keep in sync with packages/vue/src/theme/fontContext.ts */
|
|
118
118
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
119
119
|
--font-reading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
120
|
-
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", "PingFang SC", "Microsoft YaHei", monospace;
|
|
120
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/* Mobile viewports halve the floating-layer gutter. The breakpoint mirrors
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
/* Fonts — keep in sync with packages/vue/src/theme/fontContext.ts */
|
|
120
120
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
121
121
|
--font-reading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
122
|
-
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", "PingFang SC", "Microsoft YaHei", monospace;
|
|
122
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/* Mobile viewports halve the floating-layer gutter. The breakpoint mirrors
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { HIKARI_FONT_MONO, HIKARI_FONT_READING, HIKARI_FONT_SANS } from "./fontContext";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Font-stack sync contract (2026-11-07): the static SCSS tokens and the
|
|
11
|
+
* runtime fontContext constants are copies of one truth, and they
|
|
12
|
+
* drifted once — `--font-sans` carried the Linux CJK face
|
|
13
|
+
* ("Noto Sans CJK SC") in both sheet copies while `--font-mono` carried
|
|
14
|
+
* it only in fontContext.ts, so any surface rendering the STATIC sheet
|
|
15
|
+
* (pre-boot paint, or a downstream utility emitting its own literal
|
|
16
|
+
* stack) dropped CJK text into a random platform mono on Linux. All
|
|
17
|
+
* FOUR copies are pinned here — both scale.scss sheets, the fontContext
|
|
18
|
+
* constants, and the Sass-variable fallbacks in
|
|
19
|
+
* `components/_hikari-vars.scss` — so the next face can only be added
|
|
20
|
+
* in one place by failing here.
|
|
21
|
+
*
|
|
22
|
+
* Known-positive anchor: the mono stack must keep a CJK UI face AFTER the
|
|
23
|
+
* Latin mono faces and BEFORE the generic `monospace` catcher — that
|
|
24
|
+
* ordering IS the CJK-safety contract (Latin glyphs mono, CJK glyphs
|
|
25
|
+
* sans), not an accident of copy order.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
const SCSS_SOURCES = [
|
|
31
|
+
resolve(here, "../scale.scss"),
|
|
32
|
+
// Vendored byte-identical copy for npm installs (see its header).
|
|
33
|
+
resolve(here, "../styles/theme/scale.scss"),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function tokenFromScss(path: string, token: string): string {
|
|
37
|
+
const css = readFileSync(path, "utf-8");
|
|
38
|
+
const match = css.match(new RegExp(`--${token}:\\s*([^;]+);`));
|
|
39
|
+
expect(match, `${path} must declare --${token}`).toBeTruthy();
|
|
40
|
+
return match![1]!.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("font stack sync (scale.scss ↔ fontContext.ts)", () => {
|
|
44
|
+
it("keeps every vendored scale.scss copy byte-identical on the font block", () => {
|
|
45
|
+
const blocks = SCSS_SOURCES.map((path) => {
|
|
46
|
+
const css = readFileSync(path, "utf-8");
|
|
47
|
+
const match = css.match(/--font-sans:[^;]+;\s*--font-reading:[^;]+;\s*--font-mono:[^;]+;/);
|
|
48
|
+
expect(match, `${path} must declare the font token block`).toBeTruthy();
|
|
49
|
+
return match![0];
|
|
50
|
+
});
|
|
51
|
+
for (const block of blocks.slice(1)) {
|
|
52
|
+
expect(block).toBe(blocks[0]);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keeps --font-sans/--font-reading/--font-mono identical to the fontContext constants", () => {
|
|
57
|
+
for (const path of SCSS_SOURCES) {
|
|
58
|
+
expect(tokenFromScss(path, "font-sans")).toBe(HIKARI_FONT_SANS);
|
|
59
|
+
expect(tokenFromScss(path, "font-reading")).toBe(HIKARI_FONT_READING);
|
|
60
|
+
expect(tokenFromScss(path, "font-mono")).toBe(HIKARI_FONT_MONO);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps the Sass-variable fallback copies identical to the constants too", () => {
|
|
65
|
+
// _hikari-vars.scss mirrors both stacks as Sass variables for the
|
|
66
|
+
// `var(--font-*, …)` fallbacks component sheets use; normalize its
|
|
67
|
+
// single-quoted faces before comparing.
|
|
68
|
+
const normalize = (stack: string) => stack.replace(/'/g, '"').trim();
|
|
69
|
+
const vars = readFileSync(resolve(here, "../components/_hikari-vars.scss"), "utf-8");
|
|
70
|
+
const sassSans = vars.match(/\$hikari-font-family-sans:\s*([^;]+);/);
|
|
71
|
+
const sassMono = vars.match(/\$hikari-font-family-mono:\s*([^;]+);/);
|
|
72
|
+
expect(sassSans, "_hikari-vars.scss must declare the sans fallback").toBeTruthy();
|
|
73
|
+
expect(sassMono, "_hikari-vars.scss must declare the mono fallback").toBeTruthy();
|
|
74
|
+
expect(normalize(sassSans![1]!)).toBe(HIKARI_FONT_SANS);
|
|
75
|
+
expect(normalize(sassMono![1]!)).toBe(HIKARI_FONT_MONO);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("orders every mono stack CJK-safe: the Linux CJK face lands before the generic monospace catcher", () => {
|
|
79
|
+
// The generic catcher is where Linux fontconfig roulette lives — the
|
|
80
|
+
// Linux-reachable CJK face must always be resolved before the stack
|
|
81
|
+
// reaches it. Known positive: this exact predicate FAILS on the
|
|
82
|
+
// pre-2026-11-07 stack (`…"PingFang SC", "Microsoft YaHei",
|
|
83
|
+
// monospace`) — "PingFang SC" / "Microsoft YaHei" don't exist on
|
|
84
|
+
// Linux builds, so a predicate accepting ANY CJK face there would
|
|
85
|
+
// have passed the stack that caused the incident. Only the
|
|
86
|
+
// Linux-coverable face satisfies it.
|
|
87
|
+
for (const stack of [HIKARI_FONT_MONO, ...SCSS_SOURCES.map((p) => tokenFromScss(p, "font-mono")),]) {
|
|
88
|
+
const faces = stack.split(",").map((f) => f.trim().replace(/^"|"$/g, ""));
|
|
89
|
+
const catcher = faces.indexOf("monospace");
|
|
90
|
+
expect(catcher, "stack must end in the generic monospace catcher").toBe(faces.length - 1);
|
|
91
|
+
const beforeCatcher = faces.slice(0, catcher);
|
|
92
|
+
expect(
|
|
93
|
+
beforeCatcher.some((f) => /Noto Sans CJK/.test(f)),
|
|
94
|
+
`Linux CJK face must precede the monospace catcher: ${stack}`,
|
|
95
|
+
).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|