@celestia-island/hikari 0.55.54 → 0.55.55

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.54",
3
+ "version": "0.55.55",
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",
@@ -308,11 +308,12 @@ describe("HkModal publishes its own fold for the step flow", () => {
308
308
  await nextTick();
309
309
  expect(rig.morphToken()).toBe("");
310
310
 
311
- // The morph window: the freeze lifts and the override lands.
312
- rig.announceSwap({ delta: 120, durationMs: 300, phase: "morph" });
311
+ // The morph window: the freeze lifts and the override lands (the
312
+ // stepflow requests a 1ms snap — see STEP_MORPH_SNAP_MS).
313
+ rig.announceSwap({ delta: 120, durationMs: 1, phase: "morph" });
313
314
  await nextTick();
314
- expect(rig.morphToken()).toBe("300ms");
315
- await vi.advanceTimersByTimeAsync(700);
315
+ expect(rig.morphToken()).toBe("1ms");
316
+ await vi.advanceTimersByTimeAsync(500);
316
317
  expect(rig.morphToken()).toBe("");
317
318
  });
318
319
 
@@ -458,9 +459,9 @@ describe("HkModal rides its chrome and gates the slide window", () => {
458
459
  await vi.advanceTimersByTimeAsync(700);
459
460
  expect(frame.style.height).toBe("656px");
460
461
  // …and a later morph event still works (no stranded freeze).
461
- rig.announceSwap({ delta: -40, durationMs: 300, phase: "morph" });
462
+ rig.announceSwap({ delta: -40, durationMs: 1, phase: "morph" });
462
463
  await nextTick();
463
- expect(rig.morphToken()).toBe("300ms");
464
+ expect(rig.morphToken()).toBe("1ms");
464
465
  await vi.advanceTimersByTimeAsync(700);
465
466
  expect(rig.morphToken()).toBe("");
466
467
  restoreBox();
@@ -227,7 +227,7 @@ describe("HkStepFlow split-window swap (motion enabled)", () => {
227
227
  expect(t.container.querySelectorAll(".hk-stepflow-body").length).toBe(1);
228
228
  expect(t.container.querySelector(".hk-stepflow-body")?.textContent).toBe("b-body");
229
229
  expect(events).toHaveLength(2);
230
- expect(events[1]!.detail).toMatchObject({ phase: "morph", durationMs: 150 });
230
+ expect(events[1]!.detail).toMatchObject({ phase: "morph", durationMs: 1 });
231
231
  expect(typeof events[1]!.detail.delta).toBe("number");
232
232
  });
233
233
 
@@ -78,6 +78,14 @@ export const STEPFLOW_SWAP_EVENT = "hk-stepflow-swap";
78
78
  * the swap without a transitionend. */
79
79
  const SWAP_WATCHDOG_GRACE_MS = 350;
80
80
 
81
+ /** The morph window's sweep length: near-instant. A sustained clip
82
+ * animation re-rasters the moving edge per frame — the phone flicker
83
+ * the round-17 report caught in the height window even after the
84
+ * content had settled. One paint, no edge travel (the 2026-09-21
85
+ * shrink-snap decision, applied to stepflow morphs in both
86
+ * directions). */
87
+ const STEP_MORPH_SNAP_MS = 1;
88
+
81
89
  /** Fade level above which an entering body counts as the visible one. */
82
90
  const FADED_IN = 0.05;
83
91
 
@@ -216,7 +224,14 @@ export default defineComponent({
216
224
  }
217
225
 
218
226
  /** Settle the slide: recycle the old node and hand the sheet its new
219
- * height — the morph window plays alone from here. */
227
+ * height — the morph window plays alone from here. The morph
228
+ * requests a NEAR-INSTANT sweep (1ms): a sustained clip animation
229
+ * re-rasters the moving edge every frame, which on the phone GPU
230
+ * read as the height window flickering even with the content
231
+ * settled (round-17 report; the same mechanism that made shrinks
232
+ * snap in 2026-09-21). The height change still lands as its own
233
+ * window — it just completes in one paint instead of 300ms of
234
+ * edge travel. */
220
235
  function endSwap(): void {
221
236
  if (!swap) return;
222
237
  const handle = swap;
@@ -229,7 +244,7 @@ export default defineComponent({
229
244
  bodies.value = bodies.value.filter((b) => b.id !== handle.leavingId);
230
245
  announce({
231
246
  delta: newH - handle.oldH,
232
- durationMs: Math.round(handle.swapMs),
247
+ durationMs: STEP_MORPH_SNAP_MS,
233
248
  phase: "morph",
234
249
  });
235
250
  swapPhase.value = "idle";
@@ -27,12 +27,13 @@
27
27
  // top, single-line label below), and horizontal link segments join the
28
28
  // neighbouring node centers on an overlay — continuing past a side's edge
29
29
  // with a fading gradient where further hidden steps go on.
30
- .hk-timeline[data-mode="window"]:not(.hk-scroll-pin) {
31
- // The relative anchor is for the links overlay; a sticky step header
32
- // (the scroll-pin contract) is its own containing block and must keep
33
- // position: sticky — the escape keeps this rule from out-specifying
34
- // it (same fight the full-mode halo rule lost in round 16).
35
- position: relative;
30
+ //
31
+ // The LAYOUT is unconditional: scoping this rule with :not(.hk-scroll-pin)
32
+ // (as a round-16 sticky fix briefly did) deletes the whole three-column
33
+ // grid from every sticky-header timeline — the windowed wizard collapsed
34
+ // to a block stack (circles bunched left, links gone). Only the relative
35
+ // ANCHOR needs the escape, and it gets its own rule below.
36
+ .hk-timeline[data-mode="window"] {
36
37
  display: grid;
37
38
  grid-template-columns: repeat(3, minmax(0, 1fr));
38
39
  align-items: start;
@@ -45,6 +46,14 @@
45
46
  --hk-timeline-link-clearance: 20px;
46
47
  }
47
48
 
49
+ // The relative anchor for the links overlay: a sticky step header (the
50
+ // scroll-pin contract) is its own containing block and must keep
51
+ // position: sticky — the escape keeps THIS property only from
52
+ // out-specifying it (the same fight the full-mode halo rule lost).
53
+ .hk-timeline[data-mode="window"]:not(.hk-scroll-pin) {
54
+ position: relative;
55
+ }
56
+
48
57
  .hk-timeline[data-mode="window"] .hk-timeline-window {
49
58
  display: flex;
50
59
  flex-direction: column;
@@ -1,4 +1,7 @@
1
1
  import { afterEach, describe, expect, it } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { createApp, defineComponent, h, nextTick, ref } from "vue";
3
6
 
4
7
  import HkTimeline, { computeTimelineWindow } from "./HkTimeline";
@@ -487,3 +490,22 @@ describe("HkTimeline navigation motion (round 16)", () => {
487
490
  expect(host().getAttribute("data-dir")).toBe("back");
488
491
  });
489
492
  });
493
+
494
+ describe("HkTimeline window-mode layout under the sticky pin (round 17)", () => {
495
+ // Round-17 regression: scoping the whole window rule with
496
+ // :not(.hk-scroll-pin) deleted the grid layout from every
497
+ // sticky-header timeline — the windowed wizard collapsed to a block
498
+ // stack (circles left, links gone). happy-dom does not resolve the
499
+ // SCSS, so this pins the SOURCE structure: the layout rule is
500
+ // unconditional; only the relative anchor escapes.
501
+ it("keeps the window layout unconditional and escapes only position", () => {
502
+ const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "HkTimeline.scss"), "utf-8");
503
+ const layout = src.match(/\.hk-timeline\[data-mode="window"\]\s*\{([\s\S]*?)\n\}/)![1]!;
504
+ expect(layout).toContain("display: grid");
505
+ expect(layout).toContain("grid-template-columns: repeat(3, minmax(0, 1fr))");
506
+ expect(layout, "the layout rule must NOT carry the escape").not.toContain(":not(");
507
+ expect(layout, "position belongs to the escaped rule").not.toContain("position");
508
+ const anchor = src.match(/\.hk-timeline\[data-mode="window"\]:not\(\.hk-scroll-pin\)\s*\{([\s\S]*?)\n\}/)![1]!;
509
+ expect(anchor.trim()).toBe("position: relative;");
510
+ });
511
+ });