@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
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
onUnmounted,
|
|
8
8
|
ref,
|
|
9
9
|
Teleport,
|
|
10
|
-
Transition,
|
|
11
10
|
useAttrs,
|
|
12
11
|
watch,
|
|
13
12
|
type PropType,
|
|
@@ -17,6 +16,7 @@ import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
|
17
16
|
import { useBreakpoint } from "../runtime/useBreakpoint";
|
|
18
17
|
import { useI18n } from "../i18n/context";
|
|
19
18
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
19
|
+
import { useSurfaceMachine } from "../composables/useSurfaceMachine";
|
|
20
20
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
21
21
|
import { onceFrame } from "../runtime/animationBus";
|
|
22
22
|
import HIconButton from "./HkIconButton";
|
|
@@ -88,6 +88,13 @@ export default defineComponent({
|
|
|
88
88
|
|
|
89
89
|
const { isMobile } = useBreakpoint();
|
|
90
90
|
const sheetMode = computed(() => props.sheetOnMobile && isMobile.value);
|
|
91
|
+
/** The form factor this surface OPENED in. The render branches on it
|
|
92
|
+
* (not the live sheetMode) so a viewport crossing the mobile
|
|
93
|
+
* breakpoint mid-flight closes in the shape the surface was born in
|
|
94
|
+
* — the anchored popup must not flash in for the closing window of
|
|
95
|
+
* a dying sheet (or vice versa). Set on the machine's opening edge;
|
|
96
|
+
* the next open adopts whatever the viewport calls for. */
|
|
97
|
+
const activeBranch = ref<"sheet" | "anchored">("anchored");
|
|
91
98
|
|
|
92
99
|
// Crossing the mobile/desktop breakpoint mid-flight would leave a
|
|
93
100
|
// SHEET-mode panel hung between two form factors — close it and let
|
|
@@ -99,14 +106,87 @@ export default defineComponent({
|
|
|
99
106
|
});
|
|
100
107
|
|
|
101
108
|
// Open/close motion reported into the unified animation context
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
// (animationBus) — one track for the whole surface, armed on the
|
|
110
|
+
// machine's animation-phase edges.
|
|
111
|
+
const surfTrack = useSurfaceTransition(300).track("surface");
|
|
112
|
+
|
|
113
|
+
// ── Surface lifecycle machine ─────────────────────────────────────
|
|
114
|
+
// One machine drives the sheet scrim and the panel in BOTH form
|
|
115
|
+
// factors (the mobile sheet and the anchored popup) — the layers are
|
|
116
|
+
// outputs of the shared phase, so the divergent-layer states of the
|
|
117
|
+
// two-<Transition> era are unrepresentable (see
|
|
118
|
+
// runtime/surfaceMachine.ts).
|
|
119
|
+
const scrimRef = ref<HTMLElement>();
|
|
120
|
+
const machine = useSurfaceMachine({
|
|
121
|
+
layers: [
|
|
122
|
+
// Budgets are the starvation-era bounds over the SCSS truths
|
|
123
|
+
// (scrim 0.25s/0.2s, sheet panel 0.3s/0.25s, anchored pop
|
|
124
|
+
// 0.2s/0.15s); the driver probes the live CSS durations and
|
|
125
|
+
// tightens them.
|
|
126
|
+
{ prefix: "hk-popover-scrim", el: () => scrimRef.value, enterMs: () => 270, leaveMs: () => 240 },
|
|
127
|
+
// The machine classes land on the INNER panel div — the probe
|
|
128
|
+
// must read that element, not the positioning host (a classless
|
|
129
|
+
// host reads duration 0 and kills the animation in real
|
|
130
|
+
// browsers; the el-carries-the-classes invariant of #417/#418).
|
|
131
|
+
{ prefix: "hk-popover-sheet", el: () => panelRef.value, enterMs: () => 320, leaveMs: () => 280 },
|
|
132
|
+
{ prefix: "hk-popover", el: () => panelRef.value, enterMs: () => 240, leaveMs: () => 190 },
|
|
133
|
+
],
|
|
134
|
+
onPhase: (from, to, event) => {
|
|
135
|
+
if (to === "openingFrom") {
|
|
136
|
+
surfTrack.run();
|
|
137
|
+
activeBranch.value = sheetMode.value ? "sheet" : "anchored";
|
|
138
|
+
// The open arm of the old modelValue watcher, verbatim.
|
|
139
|
+
fullCleanup();
|
|
140
|
+
// Blocking follows the sheet decision: anchored (desktop) the
|
|
141
|
+
// popover is a hidden breadcrumb level, docked as a bottom
|
|
142
|
+
// sheet (mobile) it is a window layer and must be listed —
|
|
143
|
+
// hence the i18n `title` riding along.
|
|
144
|
+
handle.value = manager.register(
|
|
145
|
+
"dropdown",
|
|
146
|
+
false,
|
|
147
|
+
props.title || undefined,
|
|
148
|
+
sheetMode.value,
|
|
149
|
+
);
|
|
150
|
+
if (sheetMode.value) {
|
|
151
|
+
// Bottom sheet: nothing to anchor-measure; the scrim handles
|
|
152
|
+
// dismissal (the outside-click shield is desktop-only).
|
|
153
|
+
nextTick(() => {
|
|
154
|
+
panelRef.value?.focus?.();
|
|
155
|
+
// The sheet mounts on this render — attach the overlay
|
|
156
|
+
// once the DOM has landed (sheet panels scroll; desktop
|
|
157
|
+
// popovers size to content and need none).
|
|
158
|
+
if (machine.mounted.value && sheetMode.value && panelRef.value) {
|
|
159
|
+
detachPanelScrollbar();
|
|
160
|
+
panelScrollbar = attachOverlayScrollbars(panelRef.value, {
|
|
161
|
+
axis: "vertical",
|
|
162
|
+
host: panelHostRef.value,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
computeInitialCoords();
|
|
168
|
+
attachObservers();
|
|
169
|
+
attachOutsideClickShield();
|
|
170
|
+
nextTick(() => {
|
|
171
|
+
computePosition();
|
|
172
|
+
schedulePosition();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} else if (to === "closingFrom") {
|
|
176
|
+
surfTrack.run();
|
|
177
|
+
// The close arm of the old watcher.
|
|
178
|
+
detachObservers();
|
|
179
|
+
detachOutsideClickShield();
|
|
180
|
+
} else if (
|
|
181
|
+
to === "closed" &&
|
|
182
|
+
(from === "closingFrom" || from === "closingTo") &&
|
|
183
|
+
event !== "UNMOUNT"
|
|
184
|
+
) {
|
|
185
|
+
surfTrack.cancel();
|
|
186
|
+
onPopupAfterLeave();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
110
190
|
|
|
111
191
|
// Suppress native browser tooltips while the popover is open — and
|
|
112
192
|
// not just on the anchor itself: a `title` on ANY descendant fires
|
|
@@ -385,47 +465,9 @@ export default defineComponent({
|
|
|
385
465
|
watch(
|
|
386
466
|
() => props.modelValue,
|
|
387
467
|
(open) => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// popover is a hidden breadcrumb level, docked as a bottom
|
|
392
|
-
// sheet (mobile) it is a window layer and must be listed —
|
|
393
|
-
// hence the i18n `title` riding along.
|
|
394
|
-
handle.value = manager.register(
|
|
395
|
-
"dropdown",
|
|
396
|
-
false,
|
|
397
|
-
props.title || undefined,
|
|
398
|
-
sheetMode.value,
|
|
399
|
-
);
|
|
400
|
-
if (sheetMode.value) {
|
|
401
|
-
// Bottom sheet: nothing to anchor-measure; the scrim handles
|
|
402
|
-
// dismissal (the outside-click shield is desktop-only).
|
|
403
|
-
nextTick(() => {
|
|
404
|
-
panelRef.value?.focus?.();
|
|
405
|
-
// The sheet mounts on this render — attach the overlay
|
|
406
|
-
// once the DOM has landed (sheet panels scroll; desktop
|
|
407
|
-
// popovers size to content and need none).
|
|
408
|
-
if (props.modelValue && sheetMode.value && panelRef.value) {
|
|
409
|
-
detachPanelScrollbar();
|
|
410
|
-
panelScrollbar = attachOverlayScrollbars(panelRef.value, {
|
|
411
|
-
axis: "vertical",
|
|
412
|
-
host: panelHostRef.value,
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
} else {
|
|
417
|
-
computeInitialCoords();
|
|
418
|
-
attachObservers();
|
|
419
|
-
attachOutsideClickShield();
|
|
420
|
-
nextTick(() => {
|
|
421
|
-
computePosition();
|
|
422
|
-
schedulePosition();
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
} else {
|
|
426
|
-
detachObservers();
|
|
427
|
-
detachOutsideClickShield();
|
|
428
|
-
}
|
|
468
|
+
// The machine owns every visual/registry consequence on its
|
|
469
|
+
// phase edges; this watcher is purely the event feed.
|
|
470
|
+
machine.send(open ? "OPEN" : "CLOSE");
|
|
429
471
|
},
|
|
430
472
|
{ immediate: true },
|
|
431
473
|
);
|
|
@@ -497,7 +539,7 @@ export default defineComponent({
|
|
|
497
539
|
const panelZ = computed(() => (handle.value?.zIndex ?? 0) + 1);
|
|
498
540
|
|
|
499
541
|
const panelStyle = computed(() => {
|
|
500
|
-
if (
|
|
542
|
+
if (activeBranch.value === "sheet") {
|
|
501
543
|
// Bottom sheet: sits flush on the viewport bottom edge (the
|
|
502
544
|
// host-tunable --hk-sheet-bottom-gap lift is still honored so the
|
|
503
545
|
// sheet belongs to the HkModal/HkSelect sheet family — the family
|
|
@@ -527,49 +569,39 @@ export default defineComponent({
|
|
|
527
569
|
};
|
|
528
570
|
});
|
|
529
571
|
|
|
530
|
-
return () =>
|
|
572
|
+
return () => {
|
|
573
|
+
if (!machine.mounted.value) return null;
|
|
574
|
+
const sheetBranch = activeBranch.value === "sheet";
|
|
575
|
+
const panelPrefix = sheetBranch ? "hk-popover-sheet" : "hk-popover";
|
|
576
|
+
return (
|
|
531
577
|
<Teleport to="body">
|
|
532
578
|
{/* Window-layer contract (./_scrim-fade.scss): the sheet scrim
|
|
533
|
-
fades in place under its own transition name
|
|
534
|
-
|
|
535
|
-
around the sliding panel. z rides the live registration,
|
|
579
|
+
fades in place under its own transition name (machine output)
|
|
580
|
+
— it never mounts bare, so the dim curtain never snaps on or
|
|
581
|
+
off around the sliding panel. z rides the live registration,
|
|
536
582
|
which outlives the close until the leave fade finishes. */}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
{props.backdrop && !sheetMode.value && props.modelValue && (
|
|
583
|
+
{sheetBranch && props.closeOnBackdrop ? (
|
|
584
|
+
<div
|
|
585
|
+
ref={scrimRef}
|
|
586
|
+
class={["hk-popover-scrim", ...machine.classesFor("hk-popover-scrim")]}
|
|
587
|
+
style={{ zIndex: backdropZ.value }}
|
|
588
|
+
onClick={() => close()}
|
|
589
|
+
/>
|
|
590
|
+
) : null}
|
|
591
|
+
{props.backdrop && !sheetBranch && props.modelValue && (
|
|
547
592
|
<div
|
|
548
593
|
class="hk-popover-backdrop"
|
|
549
594
|
style={{ zIndex: backdropZ.value }}
|
|
550
595
|
/>
|
|
551
596
|
)}
|
|
552
|
-
<
|
|
553
|
-
name={sheetMode.value ? "hk-popover-sheet" : "hk-popover"}
|
|
554
|
-
appear
|
|
555
|
-
onBeforeEnter={anim.onBeforeEnter}
|
|
556
|
-
onAfterEnter={anim.onAfterEnter}
|
|
557
|
-
onBeforeLeave={anim.onBeforeLeave}
|
|
558
|
-
onAfterLeave={() => {
|
|
559
|
-
anim.onAfterLeave();
|
|
560
|
-
onPopupAfterLeave();
|
|
561
|
-
}}
|
|
562
|
-
onEnterCancelled={anim.onEnterCancelled}
|
|
563
|
-
onLeaveCancelled={anim.onLeaveCancelled}
|
|
564
|
-
>
|
|
565
|
-
{props.modelValue ? (
|
|
566
|
-
<div ref={panelHostRef} style={panelStyle.value}>
|
|
597
|
+
<div ref={panelHostRef} style={panelStyle.value}>
|
|
567
598
|
<div
|
|
568
599
|
ref={panelRef}
|
|
569
600
|
class={[
|
|
570
601
|
"hk-popover-panel",
|
|
571
|
-
...(
|
|
602
|
+
...(sheetBranch ? ["hk-is-sheet"] : [`hk-popover-${resolvedPlacement.value}`]),
|
|
572
603
|
props.glass ? "hii-dropdown-content" : "",
|
|
604
|
+
...machine.classesFor(panelPrefix),
|
|
573
605
|
...(typeof attrs.class === "string" ? [attrs.class]
|
|
574
606
|
: Array.isArray(attrs.class) ? attrs.class as string[]
|
|
575
607
|
: attrs.class && typeof attrs.class === "object"
|
|
@@ -578,15 +610,15 @@ export default defineComponent({
|
|
|
578
610
|
: []),
|
|
579
611
|
]}
|
|
580
612
|
role="dialog"
|
|
581
|
-
aria-modal={
|
|
613
|
+
aria-modal={sheetBranch ? "true" : undefined}
|
|
582
614
|
aria-label={props.title || undefined}
|
|
583
|
-
tabindex={
|
|
615
|
+
tabindex={sheetBranch ? "-1" : undefined}
|
|
584
616
|
onKeydown={onPanelKeydown}
|
|
585
617
|
>
|
|
586
|
-
{
|
|
618
|
+
{sheetBranch && (
|
|
587
619
|
<div class="hk-popover-sheet-grabber" aria-hidden="true" onClick={() => close()} />
|
|
588
620
|
)}
|
|
589
|
-
{
|
|
621
|
+
{sheetBranch && (
|
|
590
622
|
// Sheet heading row — the title and the close button on
|
|
591
623
|
// one vertically-centered line (2026-09-04 user report:
|
|
592
624
|
// the bare title band read too small and hung right of
|
|
@@ -610,10 +642,9 @@ export default defineComponent({
|
|
|
610
642
|
)}
|
|
611
643
|
{slots.default?.()}
|
|
612
644
|
</div>
|
|
613
|
-
|
|
614
|
-
) : null}
|
|
615
|
-
</Transition>
|
|
645
|
+
</div>
|
|
616
646
|
</Teleport>
|
|
617
|
-
|
|
647
|
+
);
|
|
648
|
+
};
|
|
618
649
|
},
|
|
619
650
|
});
|
|
@@ -690,14 +690,16 @@ describe("HkSelectPanel enter-class watchdog (frozen-rAF repair)", () => {
|
|
|
690
690
|
const panel = document.body.querySelector<HTMLElement>(".hk-select-sheet-panel")!;
|
|
691
691
|
expect(scrim).toBeTruthy();
|
|
692
692
|
expect(panel).toBeTruthy();
|
|
693
|
-
// The frozen enter left its from-pair on both layers
|
|
693
|
+
// The frozen enter left its from-pair on both layers (checked before
|
|
694
|
+
// any budget elapses in the frozen-clock run below).
|
|
694
695
|
expect(scrim.classList.contains("hk-select-sheet-scrim-enter-from")).toBe(true);
|
|
695
696
|
expect(panel.classList.contains("hk-select-sheet-enter-from")).toBe(true);
|
|
696
697
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
698
|
+
// Past every budget the machine rests fully open with no frozen
|
|
699
|
+
// classes — repaired without a single frame (environments with live
|
|
700
|
+
// CSS durations exercise the full window; without them the probe
|
|
701
|
+
// completes even sooner — the property is the BOUND).
|
|
702
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
701
703
|
expect(scrim.classList.contains("hk-select-sheet-scrim-enter-from")).toBe(false);
|
|
702
704
|
expect(panel.classList.contains("hk-select-sheet-enter-from")).toBe(false);
|
|
703
705
|
// …and the surface is still open and intact.
|