@celestia-island/hikari 0.55.69 → 0.55.71
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
|
@@ -32,7 +32,22 @@
|
|
|
32
32
|
--hk-stepflow-sticky-bg,
|
|
33
33
|
color-mix(in srgb, var(--hi-color-surface, #f0f4f8) 95%, transparent)
|
|
34
34
|
);
|
|
35
|
-
|
|
35
|
+
// The HOST's declared gutter owns the resting offset, not this rule. The
|
|
36
|
+
// pin contract's offset rule is (0,3,0) and parks a top pin at
|
|
37
|
+
// var(--hk-scroll-pad-top, 0px); this rule is (0,4,0), so a bare `0px`
|
|
38
|
+
// default silently out-specified it and parked the stuck pin flush
|
|
39
|
+
// against the scrollport edge instead of at the gutter line. The
|
|
40
|
+
// window-mode bonds sit 11-13px below the pin's top, so they landed
|
|
41
|
+
// INSIDE the host's own gutter cover (HkModal paints
|
|
42
|
+
// `.hk-modal-body::before` there at z-index 3), leaving their visibility
|
|
43
|
+
// resting entirely on the pin's z-index 10 staying inside that cover's
|
|
44
|
+
// stacking context. Any promoted wrapper between the scroll host and the
|
|
45
|
+
// pin collapses that: the bond measures 0 % under an opaque cover and
|
|
46
|
+
// exactly 5.0 % under a 95 % one — the round-24 phone signature
|
|
47
|
+
// (左中右三段式的步骤左右没有横线). Deferring to the gutter moves the
|
|
48
|
+
// bond to pad+11, clear of the cover band, and keeps a consumer's own
|
|
49
|
+
// override working through --hk-stepflow-sticky-top.
|
|
50
|
+
top: var(--hk-stepflow-sticky-top, var(--hk-scroll-pad-top, 0px));
|
|
36
51
|
margin-bottom: 0;
|
|
37
52
|
padding-bottom: var(--hk-stepflow-header-gap, var(--space-16, 1rem));
|
|
38
53
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract for the window-mode bond's visibility.
|
|
3
|
+
*
|
|
4
|
+
* Round-24 chest report: 左中右三段式的步骤左右没有横线 — the provider wizard's
|
|
5
|
+
* step indicator rendered its three cells but neither the completed bond,
|
|
6
|
+
* the pending bond, nor the edge continuations read as lines. Two
|
|
7
|
+
* independent causes, both measured on a real engine, both pinned here:
|
|
8
|
+
*
|
|
9
|
+
* 1. The stuck pin ignored the host's declared gutter. The step-flow's
|
|
10
|
+
* sticky rule is (0,4,0) and the pin contract's offset rule is
|
|
11
|
+
* (0,3,0), so a bare `0px` default out-specified the contract and
|
|
12
|
+
* parked the pin flush against the scrollport edge. The window-mode
|
|
13
|
+
* bonds sit 11-13px below the pin's top and therefore landed inside
|
|
14
|
+
* HkModal's own gutter cover (`.hk-modal-body::before`, z-index 3),
|
|
15
|
+
* which left their visibility resting entirely on the pin's z-index
|
|
16
|
+
* beating that cover. One promoted wrapper between the scroll host
|
|
17
|
+
* and the pin collapses it: the bond then measures 0 % under an
|
|
18
|
+
* opaque cover and exactly 5.0 % under a 95 % one.
|
|
19
|
+
* 2. The pending bond derived its colour from `--hi-color-border`, which
|
|
20
|
+
* the shipped schemes define WITH alpha (rgb(128 128 128 / 15%)), so
|
|
21
|
+
* the bond computed to 15 % × 30 % = 4.5 % — and a 2px line at 4.5 %
|
|
22
|
+
* is not a line.
|
|
23
|
+
*
|
|
24
|
+
* Both are pure stylesheet facts, which is why they are asserted against
|
|
25
|
+
* the source: jsdom runs no cascade, and the failure only shows up as
|
|
26
|
+
* pixels. The geometry half of the same investigation (the containing
|
|
27
|
+
* block is correct — the overlay anchors to the sticky timeline) needs no
|
|
28
|
+
* assertion; it was never wrong.
|
|
29
|
+
*/
|
|
30
|
+
import { describe, expect, it } from "vitest";
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const read = (name: string): string => readFileSync(join(here, name), "utf-8");
|
|
37
|
+
|
|
38
|
+
const stepflow = read("HkStepFlow.scss");
|
|
39
|
+
const timeline = read("HkTimeline.scss");
|
|
40
|
+
const scrollpin = read("HkScrollPin.scss");
|
|
41
|
+
|
|
42
|
+
/** The declaration block of the first rule whose selector starts at `from`. */
|
|
43
|
+
function blockAfter(source: string, from: number): string {
|
|
44
|
+
const end = source.indexOf("}", from);
|
|
45
|
+
return end < 0 ? source.slice(from) : source.slice(from, end);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("window-mode bond visibility contract", () => {
|
|
49
|
+
it("keeps the pin contract's gutter as the sticky step-flow header's default offset", () => {
|
|
50
|
+
// The pin contract still declares the gutter for an offset top pin...
|
|
51
|
+
expect(scrollpin).toContain("top: var(--hk-scroll-pad-top, 0px)");
|
|
52
|
+
|
|
53
|
+
// ...and the step-flow override DEFERS to it instead of replacing it
|
|
54
|
+
// with 0. Losing this is silent: the pin still pins, the cells still
|
|
55
|
+
// render, only the bonds fall under the host's gutter cover.
|
|
56
|
+
const at = stepflow.indexOf(
|
|
57
|
+
".hk-step-flow[data-sticky-header] > .hk-timeline.hk-scroll-pin",
|
|
58
|
+
);
|
|
59
|
+
expect(at).toBeGreaterThan(-1);
|
|
60
|
+
const block = blockAfter(stepflow, at);
|
|
61
|
+
expect(block).toContain(
|
|
62
|
+
"top: var(--hk-stepflow-sticky-top, var(--hk-scroll-pad-top, 0px));",
|
|
63
|
+
);
|
|
64
|
+
// The old bare default must not come back.
|
|
65
|
+
expect(block).not.toContain("top: var(--hk-stepflow-sticky-top, 0px);");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("derives the pending bond from an opaque tone so its alpha is not multiplied", () => {
|
|
69
|
+
const at = timeline.indexOf('&[data-status="pending"]');
|
|
70
|
+
expect(at).toBeGreaterThan(-1);
|
|
71
|
+
const block = blockAfter(timeline, at);
|
|
72
|
+
expect(block).toContain("--_link-color:");
|
|
73
|
+
expect(block).toContain("var(--hi-color-muted");
|
|
74
|
+
// Deriving from the border token multiplied its own alpha.
|
|
75
|
+
expect(block).not.toContain("var(--hi-color-border)");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("carries both bonds as real boxes on the shared node geometry", () => {
|
|
79
|
+
// The segments are positioned off the shared tokens, so a bond can only
|
|
80
|
+
// be invisible through colour or occlusion — never through zero size.
|
|
81
|
+
expect(timeline).toContain("--hk-timeline-link-thickness: 2px");
|
|
82
|
+
expect(timeline).toContain("--hk-timeline-link-clearance: 20px");
|
|
83
|
+
expect(timeline).toContain('&[data-segment="before-current"]');
|
|
84
|
+
expect(timeline).toContain('&[data-segment="current-after"]');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -137,7 +137,16 @@
|
|
|
137
137
|
// the current step is never a link endpoint (HkTimeline.tsx statusOf),
|
|
138
138
|
// so no "active" key here.
|
|
139
139
|
&[data-status="pending"] {
|
|
140
|
-
|
|
140
|
+
// 30 % of a SOLID muted tone. Deriving from --hi-color-border multiplied
|
|
141
|
+
// that token's own alpha: the shipped schemes define it as
|
|
142
|
+
// `rgb(128 128 128 / 15%)`, so the bond computed to 15 % × 30 % = 4.5 %
|
|
143
|
+
// — a 2px line at 4.5 % reads as no line at all, which is what the
|
|
144
|
+
// round-24 report saw on the right bond and on both edge continuations
|
|
145
|
+
// (左中右三段式的步骤左右没有横线). --hi-color-muted is an opaque text
|
|
146
|
+
// tone in the family's schemes, so the 30 % below is the strength it
|
|
147
|
+
// looks like; the intent ("a third as loud as the completed bond")
|
|
148
|
+
// survives whatever alpha a theme gives its border token.
|
|
149
|
+
--_link-color: color-mix(in srgb, var(--hi-color-muted, #6c6c6c) 30%, transparent);
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
background-color: var(--_link-color);
|
|
@@ -432,10 +441,9 @@
|
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
// ── Navigation motion (2026-09-23 user directive, round 16) ──────────
|
|
435
|
-
// The stepper never hard-cuts: status colors crossfade
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// duration/ease and collapses under reduced motion.
|
|
444
|
+
// The stepper never hard-cuts: status colors crossfade and the window-mode
|
|
445
|
+
// cells wipe in from the navigation side. All of it rides the family's
|
|
446
|
+
// standard duration/ease and collapses under reduced motion.
|
|
439
447
|
|
|
440
448
|
.hk-timeline {
|
|
441
449
|
--hk-timeline-motion: 0.3s;
|
|
@@ -453,41 +461,17 @@
|
|
|
453
461
|
border-color var(--hk-timeline-motion) var(--hk-timeline-motion-ease);
|
|
454
462
|
}
|
|
455
463
|
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
464
|
+
// The full-mode active halo was removed here (2026-09-24 user directive):
|
|
465
|
+
// a measured pill behind the active step, sliding between nodes, which
|
|
466
|
+
// read as an unexplained primary blob in every wide multi-step flow and
|
|
467
|
+
// duplicated what the filled indicator and the label weight already say.
|
|
468
|
+
// Window mode never had one. The containing-block rule below stays — it is
|
|
469
|
+
// the timeline's anchor for any absolutely positioned consumer content, and
|
|
470
|
+
// the vertical rail anchors to the step rather than to this box.
|
|
460
471
|
.hk-timeline[data-mode="full"]:not(.hk-scroll-pin) {
|
|
461
472
|
position: relative;
|
|
462
473
|
}
|
|
463
474
|
|
|
464
|
-
.hk-timeline-halo {
|
|
465
|
-
position: absolute;
|
|
466
|
-
left: 0;
|
|
467
|
-
top: 50%;
|
|
468
|
-
translate: 0 -50%;
|
|
469
|
-
border-radius: 999px;
|
|
470
|
-
background: color-mix(in srgb, var(--hi-color-primary) 10%, transparent);
|
|
471
|
-
pointer-events: none;
|
|
472
|
-
height: 100%;
|
|
473
|
-
box-sizing: border-box;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
.hk-timeline[data-orientation="vertical"] .hk-timeline-halo {
|
|
477
|
-
top: 0;
|
|
478
|
-
left: 50%;
|
|
479
|
-
translate: -50% 0;
|
|
480
|
-
width: 100%;
|
|
481
|
-
height: auto;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.hk-timeline-halo[data-placed] {
|
|
485
|
-
transition:
|
|
486
|
-
transform var(--hk-timeline-motion) var(--hk-timeline-motion-ease),
|
|
487
|
-
width var(--hk-timeline-motion) var(--hk-timeline-motion-ease),
|
|
488
|
-
height var(--hk-timeline-motion) var(--hk-timeline-motion-ease);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
475
|
// Window mode: each cell's step content is keyed by its step key, so a
|
|
492
476
|
// navigation remounts it — wipe in from the navigation side. data-dir is
|
|
493
477
|
// absent on the very first render, so opening does not animate.
|
|
@@ -553,8 +537,4 @@ html[data-css-animations="0"] .hk-timeline[data-mode="window"][data-dir] .hk-tim
|
|
|
553
537
|
.hk-timeline[data-mode="window"][data-dir] .hk-timeline-step {
|
|
554
538
|
animation: none;
|
|
555
539
|
}
|
|
556
|
-
|
|
557
|
-
.hk-timeline-halo[data-placed] {
|
|
558
|
-
transition: none;
|
|
559
|
-
}
|
|
560
540
|
}
|
|
@@ -424,36 +424,20 @@ describe("HkTimeline navigation motion (round 16)", () => {
|
|
|
424
424
|
return { container, current };
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
it("
|
|
428
|
-
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Object.defineProperty(step, "offsetLeft", {
|
|
435
|
-
configurable: true,
|
|
436
|
-
get: () => i * 100,
|
|
437
|
-
});
|
|
438
|
-
Object.defineProperty(step, "offsetWidth", {
|
|
439
|
-
configurable: true,
|
|
440
|
-
get: () => 80,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
await nextTick();
|
|
444
|
-
await nextTick();
|
|
445
|
-
const halo = container.querySelector<HTMLElement>(".hk-timeline-halo");
|
|
446
|
-
expect(halo).not.toBeNull();
|
|
447
|
-
// Active = s1 (second node): translateX(100 - pad).
|
|
448
|
-
expect(halo!.style.transform).toBe("translateX(94px)");
|
|
449
|
-
expect(halo!.style.width).toBe("92px");
|
|
450
|
-
expect(halo!.hasAttribute("data-placed")).toBe(true);
|
|
451
|
-
|
|
452
|
-
current.value = "s3";
|
|
427
|
+
it("renders no full-mode halo behind the active step", async () => {
|
|
428
|
+
// The measured pill that used to slide behind the active step is gone
|
|
429
|
+
// (2026-09-24 user directive): in a wide multi-step flow it read as an
|
|
430
|
+
// unexplained primary blob, and the filled indicator plus the label
|
|
431
|
+
// weight already carry the active step. Pinned so a later "highlight"
|
|
432
|
+
// refactor cannot quietly reintroduce it.
|
|
433
|
+
const { container } = mountTimeline(4, "s1");
|
|
453
434
|
await nextTick();
|
|
454
435
|
await nextTick();
|
|
455
|
-
|
|
456
|
-
|
|
436
|
+
expect(container.querySelector(".hk-timeline-halo")).toBeNull();
|
|
437
|
+
// The overlay the window mode owns is a different element and stays a
|
|
438
|
+
// window-mode concern — full mode draws its connectors inline.
|
|
439
|
+
expect(container.querySelector(".hk-timeline[data-mode='full']")).not.toBeNull();
|
|
440
|
+
expect(container.querySelector(".hk-timeline-links")).toBeNull();
|
|
457
441
|
});
|
|
458
442
|
|
|
459
443
|
it("marks the navigation direction for the window-mode wipe", async () => {
|
|
@@ -178,9 +178,8 @@ export default defineComponent({
|
|
|
178
178
|
|
|
179
179
|
// ── Navigation motion (2026-09-23 user directive, round 16) ──────
|
|
180
180
|
// The stepper must not hard-cut between steps: the status colors
|
|
181
|
-
// crossfade (stylesheet)
|
|
182
|
-
// navigation side (keyed remount + data-dir)
|
|
183
|
-
// active halo slides between step nodes (measured transform).
|
|
181
|
+
// crossfade (stylesheet) and the window-mode cells wipe in from the
|
|
182
|
+
// navigation side (keyed remount + data-dir).
|
|
184
183
|
let prevIndex = currentIndex.value;
|
|
185
184
|
/** Set once the first navigation revealed a direction; drives the
|
|
186
185
|
* window-mode wipe's side via the host attribute. */
|
|
@@ -192,37 +191,11 @@ export default defineComponent({
|
|
|
192
191
|
prevIndex = idx;
|
|
193
192
|
});
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!hostEl || !haloEl) return;
|
|
201
|
-
const step = hostEl.querySelector<HTMLElement>(
|
|
202
|
-
'.hk-timeline-step[data-status="active"]',
|
|
203
|
-
);
|
|
204
|
-
if (!step) return;
|
|
205
|
-
const vertical = props.orientation === "vertical";
|
|
206
|
-
const pad = 6;
|
|
207
|
-
if (vertical) {
|
|
208
|
-
haloEl.style.transform = `translateY(${Math.max(0, step.offsetTop - pad)}px)`;
|
|
209
|
-
haloEl.style.width = "";
|
|
210
|
-
haloEl.style.height = `${step.offsetHeight + pad * 2}px`;
|
|
211
|
-
} else {
|
|
212
|
-
haloEl.style.transform = `translateX(${Math.max(0, step.offsetLeft - pad)}px)`;
|
|
213
|
-
haloEl.style.height = "";
|
|
214
|
-
haloEl.style.width = `${step.offsetWidth + pad * 2}px`;
|
|
215
|
-
}
|
|
216
|
-
// The very first placement must not animate from nothing — only
|
|
217
|
-
// subsequent moves ride the CSS transition.
|
|
218
|
-
haloEl.setAttribute("data-placed", "");
|
|
219
|
-
}
|
|
220
|
-
watch(
|
|
221
|
-
() => [props.currentKey, props.orientation] as const,
|
|
222
|
-
() => {
|
|
223
|
-
void nextTick(positionHalo);
|
|
224
|
-
},
|
|
225
|
-
);
|
|
194
|
+
// The sliding full-mode highlight is gone (2026-09-24 user directive).
|
|
195
|
+
// It read as an unexplained primary blob behind the active step in every
|
|
196
|
+
// multi-step flow that is wide enough to show all of them, and the
|
|
197
|
+
// active step is already carried by its filled indicator plus the label
|
|
198
|
+
// weight. Window mode never had one, so nothing replaces it there.
|
|
226
199
|
|
|
227
200
|
function statusOf(idx: number): TimelineStepStatus {
|
|
228
201
|
return idx < currentIndex.value
|
|
@@ -262,13 +235,11 @@ export default defineComponent({
|
|
|
262
235
|
if (typeof ResizeObserver !== "undefined") {
|
|
263
236
|
observer = new ResizeObserver(() => {
|
|
264
237
|
measure();
|
|
265
|
-
positionHalo();
|
|
266
238
|
});
|
|
267
239
|
if (host.value) observer.observe(host.value);
|
|
268
240
|
}
|
|
269
241
|
void nextTick(() => {
|
|
270
242
|
measure();
|
|
271
|
-
positionHalo();
|
|
272
243
|
});
|
|
273
244
|
});
|
|
274
245
|
|
|
@@ -428,7 +399,6 @@ export default defineComponent({
|
|
|
428
399
|
data-mode="full"
|
|
429
400
|
data-dir={navDir.value ?? undefined}
|
|
430
401
|
>
|
|
431
|
-
<div ref={halo} class="hk-timeline-halo" aria-hidden="true" />
|
|
432
402
|
{props.steps.map((_step, idx) => {
|
|
433
403
|
const isLast = idx === props.steps.length - 1;
|
|
434
404
|
return renderStep(idx, { connector: !isLast, last: isLast });
|