@celestia-island/hikari 0.55.315 → 0.55.316

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.315",
3
+ "version": "0.55.316",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -26,7 +26,7 @@ function collectSourceFiles(dir: string): string[] {
26
26
  if (entry.isDirectory()) {
27
27
  if (p === path.resolve(srcDir, "animation")) continue;
28
28
  out.push(...collectSourceFiles(p));
29
- } else if (/\.(scss|ts|tsx)$/.test(entry.name) && !/\btest\.[tj]sx?$/.test(entry.name)) {
29
+ } else if (/\.(scss|ts|tsx)$/.test(entry.name) && !/\.(test|spec)\.[tj]sx?$/.test(entry.name)) {
30
30
  out.push(p);
31
31
  }
32
32
  }
@@ -179,7 +179,13 @@
179
179
  the strip itself is pointer-transparent), and inheritance does not
180
180
  beat an explicit auto. The ghost must not take clicks while it sinks:
181
181
  a tap opened a popover anchored to the fading element (2026-11-07
182
- review). Specificity (0,1,1) outranks the (0,1,0) button faces. */
182
+ review). Specificity (0,1,1) outranks the (0,1,0) button faces.
183
+ Belt relationship: pinLeaving also sets `inert` on the ghost (blocks
184
+ clicks AND focus for every descendant where the pin ran); this rule is
185
+ the CSS belt that still dead-ends buttons when the pin skipped (no
186
+ layout — tests — or a host without captured rects). Future surface
187
+ consumers whose rows carry links/inputs, not just buttons: extend this
188
+ selector or rely on the inert. */
183
189
  .hk-list-surface-leave-active button {
184
190
  pointer-events: none;
185
191
  }
@@ -122,7 +122,7 @@ describe("HkModal leave-window height pin", () => {
122
122
  it("keeps the morph pin through the whole leave and releases at finalize", async () => {
123
123
  vi.useFakeTimers();
124
124
  freezeRaf();
125
- stubPhoneViewport();
125
+ stubPhoneViewport();
126
126
  stubDurations();
127
127
  stubLayout(500);
128
128
  try {
@@ -160,7 +160,7 @@ describe("HkModal leave-window height pin", () => {
160
160
  it("leaves the frame unpinned through the enter; the open edge pins the settled height", async () => {
161
161
  vi.useFakeTimers();
162
162
  freezeRaf();
163
- stubPhoneViewport();
163
+ stubPhoneViewport();
164
164
  stubDurations();
165
165
  stubLayout(400);
166
166
  try {
@@ -197,7 +197,7 @@ describe("HkModal leave-window height pin", () => {
197
197
  it("drops the held pin at a reopen-interrupt edge; the open edge re-pins", async () => {
198
198
  vi.useFakeTimers();
199
199
  freezeRaf();
200
- stubPhoneViewport();
200
+ stubPhoneViewport();
201
201
  stubDurations();
202
202
  stubLayout(500);
203
203
  try {
@@ -378,6 +378,24 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
378
378
  }
379
379
  });
380
380
 
381
+ it("keeps the desktop rest transition to the height leg only", () => {
382
+ // The size morph's height leg is the ONLY property the desktop rest
383
+ // state ever animates. The clip-path reveal morph belongs to the
384
+ // ≤767px sheet block (which replaces the list wholesale), and
385
+ // nothing changes the frame's max-height after mount (the 70vh cap
386
+ // is the body's, a different element) — retired legs smuggled back
387
+ // into the shorthand must fail this pin (2026-11-07 residue sweep).
388
+ const rest = frameWith();
389
+ expect(winner(rest, "transition", DESKTOP)).toBe(
390
+ "height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1))",
391
+ );
392
+ // The phone sheet keeps its own clip-path-only morph list.
393
+ const mobileRest = frameWith();
394
+ expect(winner(mobileRest, "transition", MOBILE)).toBe(
395
+ "clip-path var(--hk-modal-morph-duration, 0.3s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1))",
396
+ );
397
+ });
398
+
381
399
  it("fades the frame and rides its height on one shared token pair per direction", () => {
382
400
  // Pinned WHOLE (winner() normalizes whitespace), not by prefix: a
383
401
  // fourth time value (transition-delay) or an extra smuggled segment
@@ -74,14 +74,13 @@
74
74
  // (capped) height and pins it as px on every content change — the
75
75
  // transition below animates old→new px. Both pinned values are
76
76
  // explicit px, so this needs no interpolate-size keyword support.
77
- // clip-path rides the same token pair for the mobile sheet's reveal
78
- // morph (--hk-sheet-morph: clip below): the pin lands instantly and
79
- // the top edge sweeps up paint-only — no per-frame layout, no
80
- // per-frame backdrop re-raster over the resizing fixed layer.
81
- transition:
82
- height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
83
- max-height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
84
- clip-path var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
77
+ // Height is the only leg the desktop rest state ever animates: the
78
+ // clip-path reveal morph belongs to the ≤767px sheet block (which
79
+ // replaces this list wholesale), and on desktop nothing — hikari or
80
+ // host — changes the frame's max-height after mount (the 70vh cap is
81
+ // the body's, a different element; the phone caps move by design and
82
+ // deliberately carry no leg — see the IME cap-chase note below).
83
+ transition: height var(--duration-fast, 0.15s) var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
85
84
  background: var(--hk-modal-bg, var(--hi-color-surface, rgba(255,255,255,0.95)));
86
85
  @include backdrop-filter(var(--hk-modal-blur, none));
87
86
  border: 1px solid var(--hk-modal-border-color, var(--hi-color-border, rgba(100,100,100,0.08)));
@@ -483,7 +482,7 @@
483
482
  // direction change. Growth keeps its animated reveal via the
484
483
  // clip-path sweep (the only transition left on phones).
485
484
  //
486
- // Desktop keeps the full base list (height + max-height + clip-path).
485
+ // Desktop keeps the base list (height only — the size morph's leg).
487
486
  // 0.3s = the stepflow crossfade's family standard (2026-09-21 user
488
487
  // spec, round 7): the clip sweep, the riding body and the fade must
489
488
  // land together. Keyboard-driven height changes never animate
@@ -114,16 +114,21 @@ describe("HkModal mobile sheet spacing contract", () => {
114
114
  expect(tr).not.toMatch(/\bmax-height\b/);
115
115
  });
116
116
 
117
- it("keeps the desktop frame transition list intact (base rule)", () => {
118
- // The guard is phone-only by design: the base rule outside the
119
- // media block keeps height+max-height+clip-path animated.
117
+ it("keeps the desktop frame transition to the height leg only (base rule)", () => {
118
+ // The guard is phone-only by design: outside the media block the
119
+ // base rule animates height only — the size morph's leg. The
120
+ // clip-path reveal lives in the ≤767px block (which replaces the
121
+ // list wholesale), and nothing changes the frame's max-height
122
+ // after mount (the 70vh cap is the body's, a different element):
123
+ // the 2026-11-07 residue sweep retired the never-animated
124
+ // max-height/clip-path legs, and this pin keeps them retired.
120
125
  const base = src.match(/\.hk-modal-content\s*{[^}]*}/)?.[0] ?? "";
121
126
  expect(base.length, "base .hk-modal-content rule extracted").toBeGreaterThan(0);
122
127
  const tr = base.match(/transition:\s*([^;]+);/)?.[1] ?? "";
123
128
  expect(tr.length, "base transition declaration present").toBeGreaterThan(0);
124
129
  expect(tr).toMatch(/\bheight\b/);
125
- expect(tr).toMatch(/\bmax-height\b/);
126
- expect(tr).toMatch(/\bclip-path\b/);
130
+ expect(tr).not.toMatch(/\bmax-height\b/);
131
+ expect(tr).not.toMatch(/\bclip-path\b/);
127
132
  });
128
133
 
129
134
  it("lifts the 70vh body cap on phones so the footer sits on the bottom edge", () => {
@@ -11,9 +11,11 @@
11
11
  * - growth morphs REVEAL through clip-path (--hk-sheet-morph: clip on
12
12
  * the docking surfaces, useSizeMorph does the pin-instantly +
13
13
  * sweep-up dance) — paint/compositor-level, zero per-frame layout;
14
- * the frame stylesheet carries the clip-path transition on the SAME
15
- * duration/ease tokens as the height transition, so the stretch look
16
- * and its reduced-motion / data-css-animations governance are intact
14
+ * the ≤767px sheet block replaces the frame's transition list
15
+ * wholesale and carries the clip-path transition retimed to the
16
+ * stepflow family standard, so the stretch look and its
17
+ * reduced-motion / data-css-animations governance are intact (the
18
+ * desktop base list animates the size morph's height leg only)
17
19
  * - docked phone surfaces NEVER backdrop-filter (mobile-guard tokens
18
20
  * defaulting to none: --hk-modal-blur-mobile, --hk-modal-overlay-
19
21
  * blur-mobile, --hk-drawer-blur-mobile, --hk-drawer-overlay-blur-
@@ -54,12 +56,17 @@ describe("sheet family morph-performance contract", () => {
54
56
  expect(mobileModal).toMatch(/^[ \t]*--hk-sheet-morph:[ \t]*clip;$/m);
55
57
  });
56
58
 
57
- it("carries a clip-path transition on the morph timing tokens", () => {
58
- // Same duration/ease pair as the height morph — the reveal must keep
59
- // the stretch look, not invent new timing.
59
+ it("keeps the clip sweep on the sheet block, out of the desktop base list", () => {
60
+ // 2026-11-07 residue sweep: the desktop rest state never sweeps a
61
+ // clip-path (no clip-mode surface exists ≥768px), so the base list
62
+ // carries the size morph's height leg only. The reveal grammar lives
63
+ // in the ≤767px block, which replaces the frame's transition list
64
+ // wholesale and retimes the sweep to the stepflow family standard
65
+ // (pinned by the next test).
60
66
  const frame = modal.match(/\.hk-modal-content\s*{[^}]*}/)?.[0] ?? "";
67
+ expect(frame).not.toMatch(/^[ \t]*clip-path var\(--duration-fast/m);
61
68
  expect(frame).toMatch(
62
- /^[ \t]*clip-path var\(--duration-fast, 0\.15s\) var\(--ease-standard, cubic-bezier\(0\.4, 0, 0\.2, 1\)\);$/m,
69
+ /^[ \t]*transition: height var\(--duration-fast, 0\.15s\) var\(--ease-standard, cubic-bezier\(0\.4, 0, 0\.2, 1\)\);$/m,
63
70
  );
64
71
  });
65
72
 
@@ -743,10 +743,12 @@ describe("useSizeMorph clip reveal", () => {
743
743
  // ── Leave-window hold + enter-window defer (2026-09-16 modal report) ──
744
744
  // The modal's close fold owns the frame's geometry for the whole leave:
745
745
  // hold() keeps the pin (a mid-leave content change must not resize the
746
- // folding frame) and the reopen-interrupt path animates FROM that pin.
747
- // The enter unfold is height-relative geometry (translateY 5% + bottom
748
- // 10% clip of the frame height), so deferRemeasure freezes resize-driven
749
- // re-pins through the enter and the open edge flushes them.
746
+ // folding frame); a reopen that goes hold → start without releasing
747
+ // animates from that pin (no current HkModal path does this — its reopen
748
+ // edge releases first). deferRemeasure is the generic freeze gate: while
749
+ // it returns true, resize-driven re-pins are deferred and an explicit
750
+ // remeasure() flushes them — HkModal's only remaining freezer is the
751
+ // stepflow slide window.
750
752
 
751
753
  describe("useSizeMorph hold + deferRemeasure", () => {
752
754
  it("hold keeps the pin and stops observing (leave-window stability)", () => {
@@ -790,14 +792,15 @@ describe("useSizeMorph hold + deferRemeasure", () => {
790
792
  // The initial pin is NOT gated (arming must pin immediately).
791
793
  expect(h.frame.style.height).toBe("120px");
792
794
 
793
- // Content streams in mid-enter: the RO fires but the pin must not
794
- // move — the unfold's height-relative geometry stays put.
795
+ // Content changes while the gate is closed: the RO fires but the pin
796
+ // must not move — the caller froze the frame on purpose (the
797
+ // stepflow slide window at the consumer).
795
798
  h.setNatural(160);
796
799
  FakeResizeObserver.instances[0]!.callback();
797
800
  await settle();
798
801
  expect(h.frame.style.height).toBe("120px");
799
802
 
800
- // The open edge flushes the deferred growth.
803
+ // An explicit remeasure bypasses the gate and flushes the growth.
801
804
  h.remeasure();
802
805
  expect(h.frame.style.height).toBe("160px");
803
806
 
@@ -35,12 +35,16 @@ export interface SizeMorph {
35
35
  /** Arm the morph: observe the content and pin the frame's natural
36
36
  * height on every change. Call once the surface finished its open
37
37
  * enter — the enter must own the frame's geometry (HkModal and
38
- * HkSelectPanel both arm at the machine's `open` edge; the enter
39
- * classes are off the frame by then, so calibration reads the rest
40
- * state). There is no early-arm protocol anymore: the modal's enter
41
- * is opacity+transform and never reads the frame's height, so an
42
- * enter-time pin would only freeze a stale height under
43
- * `overflow: hidden`. */
38
+ * HkSelectPanel both arm at the machine's `open` edge). The call may
39
+ * land one render flush before the enter classes actually leave the
40
+ * frame (the machine flips the phase synchronously, Vue re-renders on
41
+ * the next flush); that is safe because no enter class carries layout
42
+ * rules that differ from rest — the choreographies are
43
+ * opacity+transform, and the ≤767px from-pair's `height: auto`
44
+ * coincides with the unpinned rest height — so calibration still
45
+ * reads the rest geometry. There is no early-arm protocol anymore:
46
+ * the modal's enter never reads the frame's height, so an enter-time
47
+ * pin would only freeze a stale height under `overflow: hidden`. */
44
48
  start(): void;
45
49
  /** Disarm the morph and release the frame to `height: auto` — call
46
50
  * before a surface's leave/close so the exit animation owns the
@@ -56,7 +60,13 @@ export interface SizeMorph {
56
60
  * does this, its reopen edge releases the pin first; held content is unchanged, delta
57
61
  * zero; a re-pin to a CHANGED natural while an enter's transition
58
62
  * classes own the frame lands without a height animation, i.e. snaps
59
- * — acceptable, the enter's own choreography owns that moment). */
63
+ * — acceptable, the enter's own choreography owns that moment).
64
+ * Recorded design note: a close during an ENTER that never reached
65
+ * `open` finds the morph disarmed (hold() early-returns) and the
66
+ * leave folds on the live `height: auto` — the same unpinned grammar
67
+ * the enter plays, and preferable to folding on a stale mount-time
68
+ * pin. contentHold freezes the rendered leaf state at closingFrom, so
69
+ * only a self-updating child could resize such a fold. */
60
70
  hold(): void;
61
71
  /** Drop the height pin and hand the frame back to `height: auto`
62
72
  * without disarming anything (a hold()ed morph stays disarmed; an
@@ -177,7 +187,9 @@ export interface SizeMorphOptions {
177
187
  * shrink animation the clip-path-only transition list had lost
178
188
  * (2026-09-21 chest report, round 5) without reintroducing a frame of
179
189
  * per-frame layout. The layer promotion is RESIDENT for the whole arm
180
- * cycle (applied at start(), cleared on hold/release): promoting at the
190
+ * cycle (applied at start(), cleared by hold() at the close request; a
191
+ * mid-cycle release() — the contamination guard's re-pin —
192
+ * deliberately keeps it, see release()): promoting at the
181
193
  * step-change moment cost a one-frame see-through as the old layer died
182
194
  * before the new one rastered (same report).
183
195
  * Height-mode surfaces (desktop) keep the height morph unchanged.
@@ -233,7 +245,9 @@ export function useSizeMorph(
233
245
  * normal close. */
234
246
  let revealWatchdog: ReturnType<typeof setTimeout> | null = null;
235
247
  /** Resident layer promotion on clip-mode surfaces: applied at arm time
236
- * and cleared on hold/release. Promoting at the STEP-change moment
248
+ * and cleared by hold() at the close request (a mid-cycle release() —
249
+ * the contamination guard's re-pin — deliberately leaves it for the
250
+ * cycle to unwind, see clearResidentWill's callers). Promoting at the STEP-change moment
237
251
  * (0.55.38's per-sweep will-change) destroyed the old layer one frame
238
252
  * before the new one had rastered — on the phone GPU the sheet read
239
253
  * as a one-frame see-through flash exactly when the raster race had