@celestia-island/hikari 0.55.118 → 0.55.120

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.118",
3
+ "version": "0.55.120",
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",
@@ -434,7 +434,10 @@
434
434
  // detail sheet never carries a screenful of dead body)
435
435
  // - never grows past the top band reserved for the secondary-window
436
436
  // breadcrumb strip (--hk-sheet-top-inset; default 4.25rem keeps the
437
- // floating breadcrumb visible above a full-height sheet)
437
+ // floating breadcrumb visible above a full-height sheet, and the
438
+ // strip itself PUBLISHES this property on <body> while it is up —
439
+ // its live "own height + one gap above + one gap below" — so the
440
+ // band tracks the real chrome instead of a static guess)
438
441
  // - rounded top corners only, full width
439
442
  // ------
440
443
  @media (max-width: 767px) {
@@ -536,12 +539,16 @@
536
539
  // inside once the cap is hit); they no longer stretch it.
537
540
  // Plain-vh fallback first: dvh-less engines (older Android WebView /
538
541
  // Tauri) would drop the whole dvh calc and leave the sheet uncapped.
542
+ // !important on BOTH: an inline max-height (a host's `style` pass, a
543
+ // size prop's own cap) must lose to the family band — the strip's
544
+ // daylight is a guarantee, not a default (HkDrawer parity, 2026-10-05
545
+ // mobile report: a tall sheet covered the strip's band).
539
546
  max-height: calc(
540
547
  100vh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap, 0px)
541
- );
548
+ ) !important;
542
549
  max-height: calc(
543
550
  100dvh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap, 0px)
544
- );
551
+ ) !important;
545
552
  }
546
553
 
547
554
  // Sheet slide reveal replaces the height-clip reveal on mobile.
@@ -74,6 +74,21 @@ describe("HkModal mobile sheet spacing contract", () => {
74
74
  expect(content).not.toMatch(/max-height:\s*none/);
75
75
  });
76
76
 
77
+ // 2026-10-05 mobile report: a tall sheet covered the breadcrumb's band.
78
+ // The drawer cap has carried !important for exactly this reason (the
79
+ // inline maxHeight from a `size` prop must lose to the family band —
80
+ // HkDrawer.sheetfamily pins its side); the modal cap must not be the
81
+ // weak sibling. Any host style/prop pass writing max-height inline
82
+ // would silently uncap the sheet and swallow the strip's band.
83
+ it("beats inline max-heights with !important on both caps", () => {
84
+ // [^;]+ runs to the declaration's own semicolon, so the assertion
85
+ // sees whatever sits between the closing paren and it.
86
+ const vh = content.match(/max-height: calc\(\s*100vh - var\(--hk-sheet-top-inset[^;]+/)?.[0] ?? "";
87
+ const dvh = content.match(/max-height: calc\(\s*100dvh - var\(--hk-sheet-top-inset[^;]+/)?.[0] ?? "";
88
+ expect(vh, "plain-vh fallback cap").toContain("!important");
89
+ expect(dvh, "dvh cap").toContain("!important");
90
+ });
91
+
77
92
  // 2026-09-21 phone-sheet transition contract (two Xiaomi WebView
78
93
  // chest reports the same day): the mobile list carries ONLY the
79
94
  // clip-path reveal.
@@ -18,6 +18,9 @@ afterEach(async () => {
18
18
  manager.unregister(entry.id);
19
19
  }
20
20
  document.body.style.overflow = "";
21
+ // The strip publishes its sheet top band on <body> inline while up —
22
+ // strip it here too, or one test's band leaks into the next.
23
+ document.body.style.removeProperty("--hk-sheet-top-inset");
21
24
  window.innerWidth = originalWidth;
22
25
  // happy-dom never fires transitionend, so a popover's leave never settles
23
26
  // and its teleported panel lingers on <body> — strip it, or the next
@@ -680,6 +683,11 @@ describe("HkModalBreadcrumb overflow fold", () => {
680
683
  // zoom) / 2 = 24, and the clearance 12 + (88 visual / 2 zoom) / 2 =
681
684
  // 34 — the clearance sits lower and wins.
682
685
  expect(strip()!.style.top).toBe("44px");
686
+ // The published sheet band rides the same conversions: strip centre
687
+ // 44 + its local half-height (88 visual / 2 zoom) / 2 = 22 + one gap
688
+ // 12 — a zoom-blind band would read 78 ± (zoom−1)·Δ and drift the
689
+ // sheet cap against the strip on zoomed hosts.
690
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("78px");
683
691
  } finally {
684
692
  vi.restoreAllMocks();
685
693
  app.remove();
@@ -731,6 +739,51 @@ describe("HkModalBreadcrumb overflow fold", () => {
731
739
  }
732
740
  });
733
741
 
742
+ it("publishes the sheet top band while up and clears it with the strip", async () => {
743
+ // The strip is the authority on its own band: while it is up, mobile
744
+ // bottom sheets must stop below "strip bottom edge + one more gap" —
745
+ // the sheet family consumes --hk-sheet-top-inset as exactly that
746
+ // line, so the strip PUBLISHES it on <body> (inline beats a host's
747
+ // :root token while up) and withdraws it when the strip goes away.
748
+ // Boxless DOM → fallback geometry: band = topPx + 43/2 + gap.
749
+ const app = document.createElement("div");
750
+ app.id = "app";
751
+ document.body.appendChild(app);
752
+ const unmountAll = () => {
753
+ for (const a of mounts.splice(0)) a.unmount();
754
+ };
755
+ try {
756
+ setViewport(1200);
757
+ manager.register("modal", true, "第一层");
758
+ manager.register("modal", true, "第二层");
759
+ await mountStrip();
760
+ // 0 (app top) + max(24, 12 + 21.5) + 21.5 + 12.
761
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("67px");
762
+
763
+ // Unmounting a visible strip withdraws the band synchronously
764
+ // (onBeforeUnmount) — a stale band must not outlive its publisher
765
+ // (mutation round M5: removing the unmount clear passed everything
766
+ // else green).
767
+ unmountAll();
768
+ await nextTick();
769
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("");
770
+
771
+ // A host wanting more air raises the gap; the band follows it.
772
+ await mountStrip({ topGap: 40 });
773
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("123px");
774
+
775
+ // The promise lives with the strip: drop back to a single layer and
776
+ // the property is withdrawn — the host token rules again.
777
+ const entries = [...manager.registry.value.values()];
778
+ manager.unregister(entries[entries.length - 1].id);
779
+ await until(() => strip() === null);
780
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("");
781
+ } finally {
782
+ unmountAll();
783
+ app.remove();
784
+ }
785
+ });
786
+
734
787
  it("paints the fallback position before any resync can run", async () => {
735
788
  // A host whose app root id never resolves leaves resyncTop at its
736
789
  // early-return, so the strip paints the INITIAL ref: that line must
@@ -745,11 +798,16 @@ describe("HkModalBreadcrumb overflow fold", () => {
745
798
  manager.register("modal", true, "第二层");
746
799
  await mountStrip({ appRootId: "no-such-root" });
747
800
  expect(strip()!.style.top).toBe("33.5px");
801
+ // The band promise holds on the fallbacks too (no app root is no
802
+ // excuse for going quiet while the sheets are watching):
803
+ // 33.5 + 43/2 + 12.
804
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("67px");
748
805
  for (const a of mounts.splice(0)) a.unmount();
749
806
 
750
807
  // The init follows the prop the same way the resync does.
751
808
  await mountStrip({ appRootId: "no-such-root", topGap: 0 });
752
809
  expect(strip()!.style.top).toBe("21.5px");
810
+ expect(document.body.style.getPropertyValue("--hk-sheet-top-inset")).toBe("43px");
753
811
  });
754
812
 
755
813
  it("writes the viewport fence in the strip's own px", async () => {
@@ -407,9 +407,32 @@ export default defineComponent({
407
407
  * layout fallbacks (48px header, 43px strip). The first nextTick
408
408
  * resync replaces it with measured numbers. */
409
409
  const topPx = ref(topGapPx.value + STRIP_HEIGHT_PX / 2);
410
+
411
+ /** Publish the sheet top band: while the strip is up, mobile bottom
412
+ * sheets must stop BELOW the strip's bottom edge plus one more gap —
413
+ * "the strip's own height, with one gap above and one below". The
414
+ * sheet family consumes this exact custom property as its mobile
415
+ * max-height inset (--hk-sheet-top-inset), so publishing it here
416
+ * turns every host's static guess into the strip's live geometry.
417
+ * Written on <body> inline (beats a host's :root token while up;
418
+ * removed when the strip goes away so the host token rules again),
419
+ * in the strip's LOCAL px — the sheets teleport into the same zoomed
420
+ * subtree, so one px space is shared. */
421
+ function publishBand(bandPx: number | null): void {
422
+ if (typeof document === "undefined") return;
423
+ if (bandPx == null) document.body.style.removeProperty("--hk-sheet-top-inset");
424
+ else document.body.style.setProperty("--hk-sheet-top-inset", `${bandPx}px`);
425
+ }
426
+
410
427
  function resyncTop() {
411
428
  const app = document.getElementById(props.appRootId);
412
- if (!app) return;
429
+ if (!app) {
430
+ // No app root to measure: the strip still paints on the fallback
431
+ // geometry (the init ref), so keep the band promise on those same
432
+ // fallbacks instead of going quiet while the sheets are watching.
433
+ publishBand(topPx.value + STRIP_HEIGHT_PX / 2 + topGapPx.value);
434
+ return;
435
+ }
413
436
  // The strip teleports to <body> — inside any root CSS zoom subtree
414
437
  // — so its inline top is LOCAL px. Keep every term in that local
415
438
  // space: the app root's computed top is already local, while the
@@ -442,6 +465,9 @@ export default defineComponent({
442
465
  // viewport top can no longer push the strip's upper half against
443
466
  // (or past) the top edge.
444
467
  topPx.value = appTop + Math.max(headerH / 2, topGapPx.value + stripH / 2);
468
+ // The band: strip bottom edge plus one more gap — the lowest line a
469
+ // bottom sheet may reach while the strip is up.
470
+ publishBand(topPx.value + stripH / 2 + topGapPx.value);
445
471
  }
446
472
 
447
473
  /** Viewport fence, in the strip's OWN px. The stylesheet cap is
@@ -524,6 +550,9 @@ export default defineComponent({
524
550
  menuOpen.value = false;
525
551
  revealed.value = null;
526
552
  hiddenCount.value = 0;
553
+ // The band is the strip's live promise to the sheet family: with
554
+ // the strip gone the host's own static token rules again.
555
+ publishBand(null);
527
556
  }
528
557
  },
529
558
  { immediate: true },
@@ -553,6 +582,9 @@ export default defineComponent({
553
582
  if (handle) handle.disconnect();
554
583
  window.removeEventListener("resize", onViewportChange);
555
584
  releaseClone();
585
+ // A mounted-but-gone strip must not leave its band behind: the host's
586
+ // static token rules again (same contract as the visible flip).
587
+ publishBand(null);
556
588
  });
557
589
 
558
590
  const itemClass = (crumb: Crumb) =>