@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.69",
3
+ "version": "0.55.71",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -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
- top: var(--hk-stepflow-sticky-top, 0px);
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
- --_link-color: color-mix(in srgb, var(--hi-color-border) 30%, transparent);
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, the full-mode
436
- // active halo slides between step nodes, and the window-mode cells wipe
437
- // in from the navigation side. All of it rides the family's standard
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
- // Full mode: the active halo — a measured pill behind the active step
457
- // (the component positions it via transform/size), sliding between
458
- // nodes with the same clock. The first placement is instant (data-placed
459
- // gates the transition) so opening the sheet does not animate it.
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("positions the full-mode halo on the active step and slides it on navigation", async () => {
428
- const { container, current } = mountTimeline(4, "s1");
429
- // happy-dom has no layout engine: give the step nodes deterministic
430
- // offsets so the halo's measured transform is observable.
431
- const steps = () =>
432
- Array.from(container.querySelectorAll<HTMLElement>(".hk-timeline-step"));
433
- for (const [i, step] of steps().entries()) {
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
- // Slid to the fourth node.
456
- expect(halo!.style.transform).toBe("translateX(294px)");
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), the window-mode cells wipe in from the
182
- // navigation side (keyed remount + data-dir), and the full-mode
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
- /** The sliding full-mode highlight. */
196
- const halo = ref<HTMLElement | null>(null);
197
- function positionHalo(): void {
198
- const hostEl = host.value;
199
- const haloEl = halo.value;
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 });