@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 +1 -1
- package/src/animation/registerAnimations.test.ts +1 -1
- package/src/components/HkListTransition.scss +7 -1
- package/src/components/HkModal.leavepin.test.tsx +3 -3
- package/src/components/HkModal.open-close-scale.test.ts +18 -0
- package/src/components/HkModal.scss +8 -9
- package/src/components/HkModal.sheetgap.test.ts +10 -5
- package/src/components/HkModal.sheetmorph.test.ts +14 -7
- package/src/composables/useSizeMorph.test.ts +10 -7
- package/src/composables/useSizeMorph.ts +23 -9
package/package.json
CHANGED
|
@@ -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) &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
118
|
-
// The guard is phone-only by design: the
|
|
119
|
-
//
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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("
|
|
58
|
-
//
|
|
59
|
-
//
|
|
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]*
|
|
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)
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// re-pins
|
|
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
|
|
794
|
-
// move — the
|
|
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
|
-
//
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
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
|
|
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
|
|
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
|