@celestia-island/hikari 0.55.39 → 0.55.40

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.39",
3
+ "version": "0.55.40",
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",
@@ -410,10 +410,15 @@ describe("useSizeMorph clip reveal", () => {
410
410
  // the box keeps the OLD pin while the top edge folds down through
411
411
  // the closing inset — the pin swap lands only at the sweep's end.
412
412
  h.setNatural(320);
413
- FakeResizeObserver.instances[0]!.callback();
413
+ h.remeasure();
414
+ // Mid-warmup: the explicit inset(0) start holds (regression
415
+ // contract — never a cleared/none start).
416
+ await busFrames(1);
417
+ expect(h.frame.style.clipPath).toBe("inset(0px 0 0 0 round 0px 0px 0px 0px)");
418
+ expect(h.frame.style.height).toBe("400px");
414
419
  await settle();
415
- // Warmup + sweep in flight: clip folding toward inset(80px), height
416
- // still the old pin.
420
+ // Sweep in flight: clip folding toward inset(80px), height still
421
+ // the old pin.
417
422
  expect(h.frame.style.clipPath).toBe("inset(80px 0 0 0 round 0px 0px 0px 0px)");
418
423
  expect(h.frame.style.height).toBe("400px");
419
424
  // The sweep ends: atomic re-pin — height jumps to the target with
@@ -458,6 +463,33 @@ describe("useSizeMorph clip reveal", () => {
458
463
  expect(h.frame.style.clipPath).toBe("inset(0px 0 0 0 round 0px 0px 0px 0px)");
459
464
  });
460
465
 
466
+ it("lands the sweep via the watchdog when transitionend never fires", async () => {
467
+ const h = mountHarness(400);
468
+ h.frame.style.setProperty("--hk-sheet-morph", "clip");
469
+ h.start();
470
+
471
+ h.setNatural(320);
472
+ FakeResizeObserver.instances[0]!.callback();
473
+ await settle();
474
+ expect(h.frame.style.clipPath).toBe("inset(80px 0 0 0 round 0px 0px 0px 0px)");
475
+ expect(h.frame.style.height).toBe("400px");
476
+
477
+ // Round-6 regression: a WebView that never delivers clip-path
478
+ // transitionend used to freeze the frame here forever. The watchdog
479
+ // (duration + 350ms grace; happy-dom resolves no computed duration,
480
+ // so the 150ms fallback applies -> ~500ms) must land the same
481
+ // atomic re-pin.
482
+ await new Promise((resolve) => setTimeout(resolve, 620));
483
+ expect(h.frame.style.height).toBe("320px");
484
+ expect(h.frame.style.clipPath).toBe("");
485
+ expect(h.frame.style.willChange).toBe("clip-path");
486
+
487
+ // The watchdog is disarmed on the normal close: a later unrelated
488
+ // transitionend must be a no-op (no double landing, no throw).
489
+ fireTransitionEnd(h.frame, "clip-path");
490
+ expect(h.frame.style.height).toBe("320px");
491
+ });
492
+
461
493
  it("never clips without the mode flag (desktop height morph intact)", async () => {
462
494
  const h = mountHarness(300);
463
495
  h.start();
@@ -547,7 +579,10 @@ describe("useSizeMorph clip reveal", () => {
547
579
  h.setNatural(310);
548
580
  h.remeasure();
549
581
  expect(h.frame.style.height).toBe("380px");
550
- expect(h.frame.style.clipPath).toBe("");
582
+ // Explicit inset(0) start — the round-6 regression contract: a
583
+ // cleared (none) start point made Chromium skip the clip transition
584
+ // AND its transitionend, freezing the sheet clipped at its old pin.
585
+ expect(h.frame.style.clipPath).toBe("inset(0px 0 0 0 round 0px 0px 0px 0px)");
551
586
  await settle();
552
587
  expect(h.frame.style.clipPath).toBe("inset(70px 0 0 0 round 0px 0px 0px 0px)");
553
588
  fireTransitionEnd(h.frame, "clip-path");
@@ -160,6 +160,14 @@ export function useSizeMorph(
160
160
  let revealReport: AnimationHandle | null = null;
161
161
  let revealDir: "reveal" | "conceal" | null = null;
162
162
  let concealTo: number | null = null;
163
+ /** Sweep watchdog: transitionend is the normal closer, but a lost
164
+ * event (a WebView that skips clip-path end events entirely — the
165
+ * 2026-09-21 round-6 regression: a none→inset() start point made
166
+ * Chromium drop the transition AND its events, freezing the sheet
167
+ * clipped at its old pin) must not freeze the frame forever. The
168
+ * timer fires the exact same landing path and is disarmed on every
169
+ * normal close. */
170
+ let revealWatchdog: ReturnType<typeof setTimeout> | null = null;
163
171
  /** Resident layer promotion on clip-mode surfaces: applied at arm time
164
172
  * and cleared on hold/release. Promoting at the STEP-change moment
165
173
  * (0.55.38's per-sweep will-change) destroyed the old layer one frame
@@ -205,6 +213,10 @@ export function useSizeMorph(
205
213
  revealReport.disconnect();
206
214
  revealReport = null;
207
215
  }
216
+ if (revealWatchdog) {
217
+ clearTimeout(revealWatchdog);
218
+ revealWatchdog = null;
219
+ }
208
220
  if (revealEl && revealEnd) {
209
221
  revealEl.removeEventListener("transitionend", revealEnd);
210
222
  }
@@ -278,7 +290,13 @@ export function useSizeMorph(
278
290
  revealEl = f;
279
291
  revealEnd = onEnd;
280
292
  revealDir = dir;
281
- revealReport = reportTransition(transitionDurationMs(f));
293
+ const durationMs = transitionDurationMs(f);
294
+ revealReport = reportTransition(durationMs);
295
+ // Watchdog (see revealWatchdog): same landing, forced, if the
296
+ // transitionend never arrives.
297
+ revealWatchdog = setTimeout(() => {
298
+ if (revealEl === f && revealDir === dir) stopReveal();
299
+ }, durationMs + 350);
282
300
  f.style.clipPath =
283
301
  dir === "reveal"
284
302
  ? `inset(0px 0 0 0 round ${radii})`
@@ -423,10 +441,15 @@ export function useSizeMorph(
423
441
  f.style.clipPath = `inset(${delta}px 0 0 0 round ${radii})`;
424
442
  } else if (conceal) {
425
443
  radii = cornerRadii(f);
426
- // The box stays pinned at the OLD height (the visible start state
427
- // needs no staging — the dance-start teardown already cleared any
428
- // clip, so the box paints its full current height).
444
+ // The box stays pinned at the OLD height, and the clip start is an
445
+ // EXPLICIT inset(0) — not a cleared inline clip. Chromium does not
446
+ // interpolate clip-path between none and inset(), so a none start
447
+ // made the sweep jump to its end state and never fire
448
+ // transitionend, freezing the sheet clipped at its old pin
449
+ // (2026-09-21 round-6 regression report: "the top half is just cut
450
+ // off and then it stays there"). inset(0)→inset(N) interpolates.
429
451
  f.style.height = `${pinned}px`;
452
+ f.style.clipPath = `inset(0px 0 0 0 round ${radii})`;
430
453
  } else if (pinned > 0) {
431
454
  f.style.height = `${pinned}px`;
432
455
  }