@celestia-island/hikari 0.40.18 → 0.40.21
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/components/HkColorSchemeEditor.test.ts +24 -1
- package/src/components/HkColorSchemeEditor.tsx +19 -7
- package/src/components/HkDrawer.tsx +97 -85
- package/src/components/HkModal.enter-watchdog.test.tsx +42 -19
- package/src/components/HkModal.transition-completion.test.tsx +21 -19
- package/src/components/HkModal.tsx +91 -199
- package/src/components/HkPopover.scss +18 -0
- package/src/components/HkPopover.surfacevars.test.ts +14 -0
- package/src/components/HkPopover.tsx +120 -89
- package/src/components/HkSelectPanel.test.tsx +7 -5
- package/src/components/HkSelectPanel.tsx +163 -231
- package/src/components/HkStatusBar.test.tsx +8 -8
- package/src/components/HkThemeToggle.test.ts +6 -2
- package/src/components/scrim-fade.contract.test.ts +14 -9
- package/src/composables/useSurfaceMachine.test.ts +259 -0
- package/src/composables/useSurfaceMachine.ts +289 -0
- package/src/runtime/surfaceMachine.test.ts +186 -0
- package/src/runtime/surfaceMachine.ts +229 -0
- package/src/runtime/transitionWatchdog.test.ts +0 -79
- package/src/runtime/transitionWatchdog.ts +0 -73
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
onBeforeUnmount,
|
|
6
6
|
ref,
|
|
7
7
|
Teleport,
|
|
8
|
-
Transition,
|
|
9
8
|
watch,
|
|
10
9
|
type PropType,
|
|
11
10
|
} from "vue";
|
|
@@ -14,9 +13,9 @@ import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
|
14
13
|
import { useOverlay } from "../runtime/useOverlay";
|
|
15
14
|
import { useBreakpoint } from "../runtime/useBreakpoint";
|
|
16
15
|
import { createBackGuard } from "../runtime/backStack";
|
|
17
|
-
import { armTransitionClassWatchdog, stripTransitionClasses } from "../runtime/transitionWatchdog";
|
|
18
16
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
19
17
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
18
|
+
import { useSurfaceMachine } from "../composables/useSurfaceMachine";
|
|
20
19
|
import { useSizeMorph } from "../composables/useSizeMorph";
|
|
21
20
|
import { useI18n } from "../i18n/context";
|
|
22
21
|
import HIconButton from "./HkIconButton";
|
|
@@ -114,16 +113,14 @@ export default defineComponent({
|
|
|
114
113
|
|
|
115
114
|
const { isMobile } = useBreakpoint();
|
|
116
115
|
const sheetMode = computed(() => props.sheetOnMobile && isMobile.value);
|
|
116
|
+
/** The form factor this surface OPENED in. The render branches on
|
|
117
|
+
* it (not the live sheetMode) so a viewport crossing the mobile
|
|
118
|
+
* breakpoint mid-flight closes in the shape the surface was born
|
|
119
|
+
* in — the popout branch must not flash in for the closing window
|
|
120
|
+
* of a dying sheet (or vice versa). Set on the machine's opening
|
|
121
|
+
* edge; the next open adopts whatever the viewport calls for. */
|
|
122
|
+
const activeBranch = ref<"sheet" | "popout">("sheet");
|
|
117
123
|
|
|
118
|
-
// Open/close motion reports into the unified animation context
|
|
119
|
-
// (animationBus) for both surface shapes: the desktop popout and the
|
|
120
|
-
// mobile bottom sheet — scrim and panel on separate tracks.
|
|
121
|
-
const popoutAnim = useSurfaceTransition(250);
|
|
122
|
-
const sheetScrimAnim = useSurfaceTransition(320);
|
|
123
|
-
const sheetPanelAnim = useSurfaceTransition(320);
|
|
124
|
-
// Stable hook sets (one object per track, hoisted out of handlers).
|
|
125
|
-
const popoutHooks = popoutAnim.hooks();
|
|
126
|
-
const sheetScrimHooks = sheetScrimAnim.hooks("scrim");
|
|
127
124
|
const panelRef = ref<HTMLElement>();
|
|
128
125
|
const sheetScrimRef = ref<HTMLElement>();
|
|
129
126
|
const sheetListRef = ref<HTMLElement>();
|
|
@@ -136,60 +133,135 @@ export default defineComponent({
|
|
|
136
133
|
// content growth (a language list gaining rows, filtered options)
|
|
137
134
|
// with the height transition instead of snapping.
|
|
138
135
|
const morph = useSizeMorph(panelRef, sheetContentRef);
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
|
|
137
|
+
// Open/close motion reported into the unified animation context
|
|
138
|
+
// (animationBus) — one track for the whole surface, armed on the
|
|
139
|
+
// machine's animation-phase edges.
|
|
140
|
+
const surfTrack = useSurfaceTransition(320).track("surface");
|
|
141
|
+
|
|
142
|
+
/** Open-request bookkeeping (was the props.open watcher's open
|
|
143
|
+
* arm): popup registration (blocking follows the sheet decision),
|
|
144
|
+
* overlay registry, release-then-push back guard, and the
|
|
145
|
+
* next-tick mounts (scrollbar, duplicate-title filter). */
|
|
146
|
+
function handleOpenRequest(): void {
|
|
147
|
+
// Register with the panel title so the modal breadcrumb labels
|
|
148
|
+
// this layer by its i18n name. Blocking follows the sheet
|
|
149
|
+
// decision: the desktop popout is a hidden level, the mobile
|
|
150
|
+
// bottom sheet is a window layer that must be listed.
|
|
151
|
+
handle.value = manager.register(
|
|
152
|
+
"dropdown",
|
|
153
|
+
false,
|
|
154
|
+
props.title || undefined,
|
|
155
|
+
sheetMode.value,
|
|
156
|
+
);
|
|
157
|
+
overlay.open();
|
|
158
|
+
// Release-then-push (the HkMenu normalizer form): a same-tick
|
|
159
|
+
// close→reopen must not leave the reopened panel unguarded —
|
|
160
|
+
// release() keeps its rewind claim (desired snaps to 0) and the
|
|
161
|
+
// following push() re-advances desired by one, so the pending
|
|
162
|
+
// flush rewinds exactly to the fresh entry instead of past it.
|
|
163
|
+
if (backGuard.entries > 0) backGuard.release();
|
|
164
|
+
backGuard.push();
|
|
165
|
+
// The scrolling surface mounts on this very render (popout
|
|
166
|
+
// subtree or sheet body) — attach the overlay scrollbar once
|
|
167
|
+
// the DOM has landed. A same-tick open→close must not arm it
|
|
168
|
+
// on the leaving panel.
|
|
169
|
+
void nextTick(() => {
|
|
170
|
+
if (!props.open) return;
|
|
171
|
+
attachPanelScrollbar();
|
|
172
|
+
});
|
|
173
|
+
if (!sheetMode.value) {
|
|
174
|
+
document.addEventListener("click", onDocumentClick, true);
|
|
175
|
+
} else {
|
|
176
|
+
// The sheet body mounts on this very render — run the
|
|
177
|
+
// duplicate-title filter once it has landed, then keep
|
|
178
|
+
// re-syncing while open (async slot content swaps).
|
|
179
|
+
void nextTick(() => {
|
|
180
|
+
// A same-tick open→close must not arm the observer on the
|
|
181
|
+
// leaving panel (the close edge already ran).
|
|
182
|
+
if (!props.open || !sheetMode.value) return;
|
|
183
|
+
syncDupTitle();
|
|
184
|
+
if (sheetListRef.value && !dupTitleObserver) {
|
|
185
|
+
dupTitleObserver = new MutationObserver(syncDupTitle);
|
|
186
|
+
dupTitleObserver.observe(sheetListRef.value, {
|
|
187
|
+
childList: true,
|
|
188
|
+
characterData: true,
|
|
189
|
+
subtree: true,
|
|
190
|
+
// A consumer re-render patching className can wipe
|
|
191
|
+
// hk-sheet-dup-title mid-open — class mutations re-sync
|
|
192
|
+
// it (adding an existing class mutates nothing, so the
|
|
193
|
+
// loop converges).
|
|
194
|
+
attributes: true,
|
|
195
|
+
attributeFilter: ["class"],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
window.addEventListener("resize", onResize);
|
|
155
201
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
|
|
203
|
+
/** Close-request bookkeeping (was the watcher's close arm): the
|
|
204
|
+
* registries forget the surface at request time; the machine keeps
|
|
205
|
+
* the DOM mounted through its closing window (leave transition),
|
|
206
|
+
* painting at the remembered z band. */
|
|
207
|
+
function handleCloseRequest(): void {
|
|
208
|
+
document.removeEventListener("click", onDocumentClick, true);
|
|
209
|
+
window.removeEventListener("resize", onResize);
|
|
210
|
+
detachPanelScrollbar();
|
|
211
|
+
stopDupTitleSync();
|
|
212
|
+
// Release the pinned height so the leave owns the frame.
|
|
213
|
+
morph.stop();
|
|
214
|
+
backGuard.release();
|
|
215
|
+
if (handle.value) {
|
|
216
|
+
// Unregister immediately (stacking/breadcrumb must forget the
|
|
217
|
+
// dying panel at once) but remember its z — the closing window
|
|
218
|
+
// below keeps rendering at that band for its short lifetime
|
|
219
|
+
// instead of sinking under the page.
|
|
220
|
+
lastZ.value = handle.value.zIndex;
|
|
221
|
+
manager.unregister(handle.value.id);
|
|
222
|
+
handle.value = null;
|
|
223
|
+
}
|
|
224
|
+
overlay.close();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Surface lifecycle machine ─────────────────────────────────────
|
|
228
|
+
// One machine drives every layer of BOTH form factors — the mobile
|
|
229
|
+
// sheet (scrim + panel) and the desktop popout. The branches render
|
|
230
|
+
// disjoint elements, so the inactive layers' probes read null and
|
|
231
|
+
// only the live branch's CSS timing tightens the deadlines. The
|
|
232
|
+
// two-<Transition> era could freeze one layer's enter pair while the
|
|
233
|
+
// other stayed visible (2026-09 mobile report); with one phase the
|
|
234
|
+
// divergent states are unrepresentable. See runtime/surfaceMachine.ts
|
|
235
|
+
// for the axioms, the total table, and the invariants.
|
|
236
|
+
const machine = useSurfaceMachine({
|
|
237
|
+
layers: [
|
|
238
|
+
// Budgets are the starvation-era bounds (flip 120 + slowest
|
|
239
|
+
// layer + slack); the driver probes the live CSS durations and
|
|
240
|
+
// tightens them. SCSS truths: scrim 0.25s/0.2s, sheet panel
|
|
241
|
+
// 0.3s/0.25s, popout 0.2s/0.15s.
|
|
242
|
+
{ prefix: "hk-select-sheet-scrim", el: () => sheetScrimRef.value, enterMs: () => 270, leaveMs: () => 240 },
|
|
243
|
+
{ prefix: "hk-select-sheet", el: () => panelRef.value, enterMs: () => 320, leaveMs: () => 280 },
|
|
244
|
+
{ prefix: "hk-select-popout", el: () => popoutHostRef.value, enterMs: () => 240, leaveMs: () => 190 },
|
|
245
|
+
],
|
|
246
|
+
onPhase: (from, to) => {
|
|
247
|
+
if (to === "openingFrom") {
|
|
248
|
+
surfTrack.run();
|
|
249
|
+
activeBranch.value = sheetMode.value ? "sheet" : "popout";
|
|
250
|
+
handleOpenRequest();
|
|
251
|
+
} else if (to === "open") {
|
|
252
|
+
surfTrack.cancel();
|
|
253
|
+
// Size morphs arm once the open choreography finished —
|
|
254
|
+
// pinning during the slide-up would fight it. Harmless on the
|
|
255
|
+
// desktop popout (the sheet content probe is absent).
|
|
185
256
|
morph.start();
|
|
257
|
+
} else if (to === "closingFrom") {
|
|
258
|
+
surfTrack.run();
|
|
259
|
+
handleCloseRequest();
|
|
260
|
+
} else if (to === "closed" && (from === "closingFrom" || from === "closingTo")) {
|
|
261
|
+
surfTrack.cancel();
|
|
186
262
|
}
|
|
187
263
|
},
|
|
188
|
-
|
|
189
|
-
sheetPanelBaseHooks.onBeforeLeave();
|
|
190
|
-
morph.stop();
|
|
191
|
-
},
|
|
192
|
-
};
|
|
264
|
+
});
|
|
193
265
|
|
|
194
266
|
// Window-first back priority (HkModal convention): while this panel is
|
|
195
267
|
// the topmost window, the back gesture closes it instead of navigating
|
|
@@ -221,12 +293,14 @@ export default defineComponent({
|
|
|
221
293
|
if (props.open && sheetMode.value) syncDupTitle();
|
|
222
294
|
},
|
|
223
295
|
);
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
296
|
+
// A consumer-prop flip out of sheet mode while open (the breakpoint
|
|
297
|
+
// flip closes the panel instead) keeps rendering the BORN branch —
|
|
298
|
+
// only the registry tracks the new form. The dup-title observer
|
|
299
|
+
// drops promptly so it stops guarding a soon-irrelevant subtree
|
|
300
|
+
// (the sheet list stays mounted through the rest of that open
|
|
301
|
+
// cycle, but re-syncing its duplicate headings no longer matters
|
|
302
|
+
// once the form is no longer the sheet). Blocking follows so the
|
|
303
|
+
// breadcrumb level tracks the surface's declared form.
|
|
230
304
|
watch(sheetMode, (mode) => {
|
|
231
305
|
if (!mode) stopDupTitleSync();
|
|
232
306
|
if (handle.value) manager.setBlocking(handle.value.id, mode);
|
|
@@ -371,85 +445,13 @@ export default defineComponent({
|
|
|
371
445
|
watch(
|
|
372
446
|
() => props.open,
|
|
373
447
|
(open) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// decision: the desktop popout is a hidden level, the mobile
|
|
378
|
-
// bottom sheet is a window layer that must be listed.
|
|
379
|
-
handle.value = manager.register(
|
|
380
|
-
"dropdown",
|
|
381
|
-
false,
|
|
382
|
-
props.title || undefined,
|
|
383
|
-
sheetMode.value,
|
|
384
|
-
);
|
|
385
|
-
overlay.open();
|
|
386
|
-
// Release-then-push (the HkMenu normalizer form): a same-tick
|
|
387
|
-
// close→reopen must not leave the reopened panel unguarded —
|
|
388
|
-
// release() keeps its rewind claim (desired snaps to 0) and the
|
|
389
|
-
// following push() re-advances desired by one, so the pending
|
|
390
|
-
// flush rewinds exactly to the fresh entry instead of past it.
|
|
391
|
-
if (backGuard.entries > 0) backGuard.release();
|
|
392
|
-
backGuard.push();
|
|
393
|
-
// The scrolling surface mounts on this very render (popout
|
|
394
|
-
// subtree or sheet body) — attach the overlay scrollbar once
|
|
395
|
-
// the DOM has landed. A same-tick open→close must not arm it
|
|
396
|
-
// on the leaving panel.
|
|
397
|
-
void nextTick(() => {
|
|
398
|
-
if (!props.open) return;
|
|
399
|
-
attachPanelScrollbar();
|
|
400
|
-
});
|
|
401
|
-
if (!sheetMode.value) {
|
|
402
|
-
document.addEventListener("click", onDocumentClick, true);
|
|
403
|
-
} else {
|
|
404
|
-
// The sheet body mounts on this very render — run the
|
|
405
|
-
// duplicate-title filter once it has landed, then keep
|
|
406
|
-
// re-syncing while open (async slot content swaps).
|
|
407
|
-
void nextTick(() => {
|
|
408
|
-
// A same-tick open→close must not arm the observer on the
|
|
409
|
-
// leaving panel (the close branch already ran).
|
|
410
|
-
if (!props.open || !sheetMode.value) return;
|
|
411
|
-
syncDupTitle();
|
|
412
|
-
if (sheetListRef.value && !dupTitleObserver) {
|
|
413
|
-
dupTitleObserver = new MutationObserver(syncDupTitle);
|
|
414
|
-
dupTitleObserver.observe(sheetListRef.value, {
|
|
415
|
-
childList: true,
|
|
416
|
-
characterData: true,
|
|
417
|
-
subtree: true,
|
|
418
|
-
// A consumer re-render patching className can wipe
|
|
419
|
-
// hk-sheet-dup-title mid-open — class mutations
|
|
420
|
-
// re-sync it (adding an existing class mutates
|
|
421
|
-
// nothing, so the loop converges).
|
|
422
|
-
attributes: true,
|
|
423
|
-
attributeFilter: ["class"],
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
window.addEventListener("resize", onResize);
|
|
429
|
-
} else {
|
|
430
|
-
document.removeEventListener("click", onDocumentClick, true);
|
|
431
|
-
window.removeEventListener("resize", onResize);
|
|
432
|
-
detachPanelScrollbar();
|
|
433
|
-
stopDupTitleSync();
|
|
434
|
-
// Enter cycles died with the open state — their guards must
|
|
435
|
-
// not fire into the leave transitions.
|
|
436
|
-
disarmEnterGuards();
|
|
437
|
-
backGuard.release();
|
|
438
|
-
if (handle.value) {
|
|
439
|
-
// Unregister immediately (stacking/breadcrumb must forget the
|
|
440
|
-
// dying panel at once) but remember its z — the leave
|
|
441
|
-
// transition below keeps rendering at that band for its
|
|
442
|
-
// short lifetime instead of sinking under the page.
|
|
443
|
-
lastZ.value = handle.value.zIndex;
|
|
444
|
-
manager.unregister(handle.value.id);
|
|
445
|
-
handle.value = null;
|
|
446
|
-
}
|
|
447
|
-
overlay.close();
|
|
448
|
-
}
|
|
448
|
+
// The machine owns every visual/registry consequence on its
|
|
449
|
+
// phase edges; this watcher is purely the event feed.
|
|
450
|
+
machine.send(open ? "OPEN" : "CLOSE");
|
|
449
451
|
},
|
|
450
|
-
// immediate: mounting with open=true must
|
|
451
|
-
//
|
|
452
|
-
//
|
|
452
|
+
// immediate: mounting with open=true must walk the machine into
|
|
453
|
+
// its opening edge right away — the HkPopover / HkModal
|
|
454
|
+
// convention.
|
|
453
455
|
{ immediate: true },
|
|
454
456
|
);
|
|
455
457
|
|
|
@@ -458,7 +460,6 @@ export default defineComponent({
|
|
|
458
460
|
window.removeEventListener("resize", onResize);
|
|
459
461
|
detachPanelScrollbar();
|
|
460
462
|
stopDupTitleSync();
|
|
461
|
-
disarmEnterGuards();
|
|
462
463
|
overlay.close();
|
|
463
464
|
backGuard.destroy();
|
|
464
465
|
if (handle.value) {
|
|
@@ -560,11 +561,12 @@ export default defineComponent({
|
|
|
560
561
|
});
|
|
561
562
|
|
|
562
563
|
return () => {
|
|
563
|
-
//
|
|
564
|
-
// leave transition (slide-down) actually
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
if (
|
|
564
|
+
// The machine keeps the Teleport mounted across the whole closing
|
|
565
|
+
// window so the leave transition (slide-down / pop-out) actually
|
|
566
|
+
// runs — `closed` unmounts it (the same reason the pre-machine
|
|
567
|
+
// code kept its Transitions alive through the leave).
|
|
568
|
+
if (!machine.mounted.value) return null;
|
|
569
|
+
if (activeBranch.value === "sheet") {
|
|
568
570
|
return (
|
|
569
571
|
<Teleport to="body">
|
|
570
572
|
{/* Window-layer contract (./_scrim-fade.scss): the scrim fades
|
|
@@ -572,49 +574,15 @@ export default defineComponent({
|
|
|
572
574
|
panel's `hk-select-sheet` name, so the panel's
|
|
573
575
|
translateY(100%) enter pair slid the dim curtain up from
|
|
574
576
|
the bottom edge on phones (2026-09-06 report). */}
|
|
575
|
-
<
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
scrimEnterGuard = armTransitionClassWatchdog(
|
|
583
|
-
el as HTMLElement,
|
|
584
|
-
"hk-select-sheet-scrim",
|
|
585
|
-
() => props.open,
|
|
586
|
-
);
|
|
587
|
-
}}
|
|
588
|
-
onAfterEnter={(el: Element) => {
|
|
589
|
-
sheetScrimHooks.onAfterEnter();
|
|
590
|
-
if (el === sheetScrimRef.value) {
|
|
591
|
-
scrimEnterGuard?.();
|
|
592
|
-
scrimEnterGuard = null;
|
|
593
|
-
}
|
|
594
|
-
}}
|
|
595
|
-
onEnterCancelled={() => {
|
|
596
|
-
sheetScrimHooks.onEnterCancelled();
|
|
597
|
-
scrimEnterGuard?.();
|
|
598
|
-
scrimEnterGuard = null;
|
|
599
|
-
}}
|
|
600
|
-
onBeforeLeave={sheetScrimHooks.onBeforeLeave}
|
|
601
|
-
onAfterLeave={sheetScrimHooks.onAfterLeave}
|
|
602
|
-
onLeaveCancelled={sheetScrimHooks.onLeaveCancelled}
|
|
603
|
-
>
|
|
604
|
-
{props.open ? (
|
|
605
|
-
<div
|
|
606
|
-
ref={sheetScrimRef}
|
|
607
|
-
class="hk-select-sheet-scrim"
|
|
608
|
-
style={{ zIndex: popoutZ.value - 1 }}
|
|
609
|
-
onClick={close}
|
|
610
|
-
/>
|
|
611
|
-
) : null}
|
|
612
|
-
</Transition>
|
|
613
|
-
<Transition name="hk-select-sheet" appear {...sheetPanelHooks}>
|
|
614
|
-
{props.open ? (
|
|
615
|
-
<div
|
|
577
|
+
<div
|
|
578
|
+
ref={sheetScrimRef}
|
|
579
|
+
class={["hk-select-sheet-scrim", ...machine.classesFor("hk-select-sheet-scrim")]}
|
|
580
|
+
style={{ zIndex: popoutZ.value - 1 }}
|
|
581
|
+
onClick={close}
|
|
582
|
+
/>
|
|
583
|
+
<div
|
|
616
584
|
ref={panelRef}
|
|
617
|
-
class="hk-select-sheet-panel"
|
|
585
|
+
class={["hk-select-sheet-panel", ...machine.classesFor("hk-select-sheet")]}
|
|
618
586
|
style={{ zIndex: popoutZ.value }}
|
|
619
587
|
role="dialog"
|
|
620
588
|
aria-modal="true"
|
|
@@ -655,8 +623,6 @@ export default defineComponent({
|
|
|
655
623
|
</div>
|
|
656
624
|
</div>
|
|
657
625
|
</div>
|
|
658
|
-
) : null}
|
|
659
|
-
</Transition>
|
|
660
626
|
</Teleport>
|
|
661
627
|
);
|
|
662
628
|
}
|
|
@@ -671,41 +637,9 @@ export default defineComponent({
|
|
|
671
637
|
// after an auto-flip).
|
|
672
638
|
return (
|
|
673
639
|
<Teleport to="body">
|
|
674
|
-
<
|
|
675
|
-
name="hk-select-popout"
|
|
676
|
-
appear
|
|
677
|
-
onBeforeEnter={(el: Element) => {
|
|
678
|
-
popoutHooks.onBeforeEnter();
|
|
679
|
-
stripTransitionClasses(el as HTMLElement, "hk-select-popout");
|
|
680
|
-
popoutEnterGuard?.();
|
|
681
|
-
popoutEnterGuard = armTransitionClassWatchdog(
|
|
682
|
-
el as HTMLElement,
|
|
683
|
-
"hk-select-popout",
|
|
684
|
-
() => props.open,
|
|
685
|
-
);
|
|
686
|
-
}}
|
|
687
|
-
onAfterEnter={(el: Element) => {
|
|
688
|
-
popoutHooks.onAfterEnter();
|
|
689
|
-
// Identity-gated disarm: a stale after-enter from an
|
|
690
|
-
// interrupted cycle must not disarm the live cycle's guard.
|
|
691
|
-
if (el === popoutHostRef.value) {
|
|
692
|
-
popoutEnterGuard?.();
|
|
693
|
-
popoutEnterGuard = null;
|
|
694
|
-
}
|
|
695
|
-
}}
|
|
696
|
-
onEnterCancelled={() => {
|
|
697
|
-
popoutHooks.onEnterCancelled();
|
|
698
|
-
popoutEnterGuard?.();
|
|
699
|
-
popoutEnterGuard = null;
|
|
700
|
-
}}
|
|
701
|
-
onBeforeLeave={popoutHooks.onBeforeLeave}
|
|
702
|
-
onAfterLeave={popoutHooks.onAfterLeave}
|
|
703
|
-
onLeaveCancelled={popoutHooks.onLeaveCancelled}
|
|
704
|
-
>
|
|
705
|
-
{props.open ? (
|
|
706
|
-
<div
|
|
640
|
+
<div
|
|
707
641
|
ref={popoutHostRef}
|
|
708
|
-
class="hk-select-popout-host"
|
|
642
|
+
class={["hk-select-popout-host", ...machine.classesFor("hk-select-popout")]}
|
|
709
643
|
data-side={resolved.value.side}
|
|
710
644
|
data-align={resolved.value.align}
|
|
711
645
|
style={{ ...coords.value, zIndex: popoutZ.value }}
|
|
@@ -719,8 +653,6 @@ export default defineComponent({
|
|
|
719
653
|
{slots.default?.()}
|
|
720
654
|
</div>
|
|
721
655
|
</div>
|
|
722
|
-
) : null}
|
|
723
|
-
</Transition>
|
|
724
656
|
</Teleport>
|
|
725
657
|
);
|
|
726
658
|
};
|
|
@@ -142,14 +142,14 @@ describe("HkStatusBar", () => {
|
|
|
142
142
|
tag.dispatchEvent(tap());
|
|
143
143
|
await nextTick();
|
|
144
144
|
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(
|
|
152
|
-
expect(
|
|
145
|
+
// Machine era: in no-transition environments (happy-dom's computed
|
|
146
|
+
// durations read 0) the close settles on the microtask queue — the
|
|
147
|
+
// rAF-era lingering leave window is gone. Assert the dismissal
|
|
148
|
+
// completed: the panel unmounted, exactly one retry ever fired.
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
150
|
+
await nextTick();
|
|
151
|
+
expect(document.body.querySelector(".hk-popover-panel")).toBeNull();
|
|
152
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
it("a scroll-like touch ending over the tag does not retry", async () => {
|
|
@@ -299,12 +299,16 @@ describe("HkThemeToggle item slots", () => {
|
|
|
299
299
|
const rows = [...document.body.querySelectorAll<HTMLElement>(".s-theme-menu .s-theme-item-row")];
|
|
300
300
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
301
301
|
// Every row — active or not — carries the slot cell; no check cell is
|
|
302
|
-
// rendered alongside (the slot owns the leading zone).
|
|
302
|
+
// rendered alongside (the slot owns the leading zone). Exactly ONE
|
|
303
|
+
// slot cell per row in the DOM — the machine-era popover re-renders
|
|
304
|
+
// per phase (class outputs are reactive), so slot invocation counts
|
|
305
|
+
// are no longer 1:1 with rows; DOM uniqueness is the invariant.
|
|
303
306
|
for (const row of rows) {
|
|
304
307
|
expect(row.querySelector(".s-theme-item-lead .lead-mark")).toBeTruthy();
|
|
308
|
+
expect(row.querySelectorAll(".lead-mark")).toHaveLength(1);
|
|
305
309
|
expect(row.querySelector(".s-theme-item-check")).toBeNull();
|
|
306
310
|
}
|
|
307
|
-
expect(seen.length).
|
|
311
|
+
expect(seen.length).toBeGreaterThanOrEqual(rows.length);
|
|
308
312
|
});
|
|
309
313
|
|
|
310
314
|
it("reserves the trailing column and suppresses the built-in delete overlay", async () => {
|
|
@@ -63,20 +63,25 @@ describe("window-layer scrim fade contract", () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
it("the select sheet scrim rides a transition name distinct from the panel's", () => {
|
|
66
|
+
// Machine era: the surface lifecycle is driven by useSurfaceMachine
|
|
67
|
+
// whose layer prefixes ARE the transition names — the contract
|
|
68
|
+
// survives as the distinct-prefix wiring (scrim never shares the
|
|
69
|
+
// panel's slide name).
|
|
66
70
|
const src = read("HkSelectPanel.tsx");
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
expect(
|
|
71
|
-
expect(names).toContain("hk-select-sheet");
|
|
72
|
-
expect(src).toContain('sheetScrimAnim.hooks("scrim")');
|
|
71
|
+
expect(src).toContain('prefix: "hk-select-sheet-scrim"');
|
|
72
|
+
expect(src).toContain('prefix: "hk-select-sheet"');
|
|
73
|
+
expect(src).toContain('prefix: "hk-select-popout"');
|
|
74
|
+
expect(src).toContain("useSurfaceMachine({");
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
it("the popover sheet scrim fades instead of mounting bare", () => {
|
|
78
|
+
// Machine era: the scrim's fade classes are machine outputs under a
|
|
79
|
+
// prefix DISTINCT from the panel's slide names — the contract
|
|
80
|
+
// survives as the layer wiring.
|
|
76
81
|
const src = read("HkPopover.tsx");
|
|
77
|
-
expect(src).toContain('
|
|
78
|
-
expect(src).toContain(
|
|
82
|
+
expect(src).toContain('prefix: "hk-popover-scrim"');
|
|
83
|
+
expect(src).toContain("useSurfaceMachine({");
|
|
79
84
|
// The panel's sheet name stays the slide family — distinct from the scrim.
|
|
80
|
-
expect(src).toContain('"hk-popover-sheet"');
|
|
85
|
+
expect(src).toContain('prefix: "hk-popover-sheet"');
|
|
81
86
|
});
|
|
82
87
|
});
|