@celestia-island/hikari 0.55.307 → 0.55.309
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
|
@@ -332,7 +332,14 @@ export default defineComponent({
|
|
|
332
332
|
// finding). Riding the block keeps the scrollport's content
|
|
333
333
|
// untransformed; the block's own poke below the frame lands
|
|
334
334
|
// off-screen on a bottom-docked sheet.
|
|
335
|
-
|
|
335
|
+
// While the desktop height ride owns the body's paint (its scaleY
|
|
336
|
+
// IS the choreography), the morph must not write an inline transform
|
|
337
|
+
// on the same element: an inline style outranks the ride's class
|
|
338
|
+
// rule, and the close would stop compressing (2026-11-07 user
|
|
339
|
+
// report: the window folded "together with the chrome"). The body
|
|
340
|
+
// ride matters for the ≤767px sheet sweep, where the ride geometry
|
|
341
|
+
// is disabled — so the two never legitimately overlap.
|
|
342
|
+
if (bodyRef.value && rideStateClass() === null) out.push({ el: bodyRef.value });
|
|
336
343
|
return out;
|
|
337
344
|
},
|
|
338
345
|
// Republish the morph's real sweep span and its landing on the body
|
|
@@ -1126,6 +1133,7 @@ export default defineComponent({
|
|
|
1126
1133
|
<div
|
|
1127
1134
|
ref={contentRef}
|
|
1128
1135
|
class={["hk-modal-content", "hk-ride", rideStateClass(), props.contentClass, ...machine.classesFor("hk-modal-content")]}
|
|
1136
|
+
data-hk-ride-ready={ride.hasCache() ? "" : undefined}
|
|
1129
1137
|
role="dialog"
|
|
1130
1138
|
aria-modal="true"
|
|
1131
1139
|
aria-label={resolvedSurfaceName.value}
|
|
@@ -1133,6 +1141,8 @@ export default defineComponent({
|
|
|
1133
1141
|
style={{
|
|
1134
1142
|
maxWidth: resolvedWidth.value,
|
|
1135
1143
|
zIndex: contentZ.value,
|
|
1144
|
+
...ride.styleVars(),
|
|
1145
|
+
|
|
1136
1146
|
}}
|
|
1137
1147
|
onClick={onContentClick}
|
|
1138
1148
|
tabindex={-1}
|
|
@@ -862,6 +862,35 @@ describe("HkRangeRibbon reset", () => {
|
|
|
862
862
|
expect(second.defaultPrevented).toBe(true);
|
|
863
863
|
});
|
|
864
864
|
|
|
865
|
+
it("treats a full-domain window as nothing to reset (raw-records rest state)", async () => {
|
|
866
|
+
// A minimal-style host rests at the controlled FULL selection [0, 1]:
|
|
867
|
+
// the grips paint, but there is no narrower selection to come back to.
|
|
868
|
+
// Escape there must reach the host (the dialog closes), not "reset" the
|
|
869
|
+
// bar it already shows — before 2026-11-07 every press swallowed the key.
|
|
870
|
+
const host = mountHost([0, 1]);
|
|
871
|
+
const strip = host.root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!;
|
|
872
|
+
const event = keydown(strip, "Escape");
|
|
873
|
+
await nextTick();
|
|
874
|
+
expect(host.windowChange, "full selection: nothing resets").not.toHaveBeenCalled();
|
|
875
|
+
expect(event.defaultPrevented, "a host dialog must still see Escape").toBe(false);
|
|
876
|
+
|
|
877
|
+
// The reset BUTTON agrees with Escape: nothing to reset at [0, 1].
|
|
878
|
+
expect(host.root.querySelector(".hk-range-ribbon-reset")).toBeNull();
|
|
879
|
+
// The grips stay (they are the selection itself, not a reset affordance).
|
|
880
|
+
expect(host.root.querySelector(".hk-range-ribbon-handle")).not.toBeNull();
|
|
881
|
+
|
|
882
|
+
// A real sub-domain still resets — and the chip is back for it.
|
|
883
|
+
await host.setWindow([0.3, 0.7]);
|
|
884
|
+
expect(host.root.querySelector(".hk-range-ribbon-reset")).not.toBeNull();
|
|
885
|
+
const second = keydown(host.root.querySelector<HTMLElement>(".hk-range-ribbon-strip")!, "Escape");
|
|
886
|
+
await nextTick();
|
|
887
|
+
expect(host.windowChange).toHaveBeenLastCalledWith(null);
|
|
888
|
+
expect(second.defaultPrevented).toBe(true);
|
|
889
|
+
// Back at the full selection the chip is gone again.
|
|
890
|
+
await host.setWindow([0, 1]);
|
|
891
|
+
expect(host.root.querySelector(".hk-range-ribbon-reset")).toBeNull();
|
|
892
|
+
});
|
|
893
|
+
|
|
865
894
|
it("cancels an in-flight brush with Escape and consumes the key", async () => {
|
|
866
895
|
const host = mountHost(null);
|
|
867
896
|
const { strip, x } = stubStrip(host.root);
|
|
@@ -1496,10 +1525,13 @@ describe("HkRangeRibbon stylesheet contract", () => {
|
|
|
1496
1525
|
});
|
|
1497
1526
|
|
|
1498
1527
|
// Wave (2026-11-06 raw-records five): MINIMAL MODE - the bare dual-handle
|
|
1499
|
-
// bar. Contract: data-minimal on the root, strip present
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1528
|
+
// bar. Contract: data-minimal on the root, strip present, ZERO lane chrome
|
|
1529
|
+
// (no lanes block, no lane rows, no overview minimap, no gutter probe),
|
|
1530
|
+
// and the standard mode keeps its lane chrome + alignment probe. The
|
|
1531
|
+
// uncontrolled default window is NULL here — the grips need a controlled
|
|
1532
|
+
// non-null window (the chest raw-records adapter feeds [0, 1] for its
|
|
1533
|
+
// "default = everything selected" reading); the aria readings below come
|
|
1534
|
+
// from the strip's null-window fallbacks, not from a real window.
|
|
1503
1535
|
it("minimal mode renders the bare dual-handle bar (no lane chrome)", () => {
|
|
1504
1536
|
const root = mountRibbon({ minimal: true });
|
|
1505
1537
|
expect(root.getAttribute("data-minimal")).toBe("true");
|
|
@@ -1507,8 +1539,9 @@ describe("HkRangeRibbon stylesheet contract", () => {
|
|
|
1507
1539
|
expect(stripEl).not.toBeNull();
|
|
1508
1540
|
// The strip carries the slider a11y (role/tabindex/label/valuenow).
|
|
1509
1541
|
expect(stripEl.getAttribute("aria-label")).toBe("Observation range");
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1542
|
+
// No window ⇒ the strip's aria falls back to the full range ("0" /
|
|
1543
|
+
// "0% – 100%"): a truthful reading for an unfiltered bar, even though
|
|
1544
|
+
// no grips paint without a controlled window.
|
|
1512
1545
|
expect(stripEl.getAttribute("aria-valuenow")).toBe("0");
|
|
1513
1546
|
expect(stripEl.getAttribute("aria-valuetext")).toContain("0%");
|
|
1514
1547
|
// The lanes BLOCK is skipped entirely (no lane rows at all).
|
|
@@ -1522,7 +1555,6 @@ describe("HkRangeRibbon stylesheet contract", () => {
|
|
|
1522
1555
|
// static scss assertion at the end of this file — happy-dom cannot
|
|
1523
1556
|
// resolve the nested attribute selector.)
|
|
1524
1557
|
expect(root.querySelector(".hk-range-ribbon-overview-segment")).toBeNull();
|
|
1525
|
-
// Uncontrolled default window = the FULL domain (edge to edge).
|
|
1526
1558
|
// Standard consumers keep the lane chrome + the alignment probe.
|
|
1527
1559
|
const std = mountRibbon({});
|
|
1528
1560
|
expect(std.querySelector(".hk-range-ribbon-lane")).not.toBeNull();
|
|
@@ -264,9 +264,12 @@ export default defineComponent({
|
|
|
264
264
|
* range bar. No lane rows, no overview minimap, no label gutter; the
|
|
265
265
|
* strip spans the full width and its track uses muted-based tokens
|
|
266
266
|
* (the surface-based track read whitish inside dark phone modals).
|
|
267
|
-
* The
|
|
268
|
-
* minimal bar starts
|
|
269
|
-
*
|
|
267
|
+
* The grips paint only for a NON-null window, so an uncontrolled
|
|
268
|
+
* minimal bar starts as a bare track; a host that wants the "default =
|
|
269
|
+
* everything selected" reading feeds the controlled window `[0, 1]`
|
|
270
|
+
* (the chest raw-records adapter does exactly that).
|
|
271
|
+
* `segments` / `selectedId` are IGNORED in this mode (no segment DOM to
|
|
272
|
+
* select from). */
|
|
270
273
|
minimal: { type: Boolean, default: false },
|
|
271
274
|
},
|
|
272
275
|
emits: {
|
|
@@ -350,9 +353,21 @@ export default defineComponent({
|
|
|
350
353
|
return true;
|
|
351
354
|
}
|
|
352
355
|
|
|
353
|
-
/**
|
|
354
|
-
*
|
|
356
|
+
/** A window that covers the whole domain IS the unfiltered state — there
|
|
357
|
+
* is nothing to reset back to (2026-11-07, the raw-records consumer: a
|
|
358
|
+
* minimal bar paints its grips for a non-null window, so its rest state
|
|
359
|
+
* is the FULL selection `[0, 1]`; without this check Escape "reset" it
|
|
360
|
+
* every press, swallowed the key, and the hosting dialog could never
|
|
361
|
+
* close while the bar held focus). */
|
|
362
|
+
function isFullDomain(win: WindowRange | null): boolean {
|
|
363
|
+
return win !== null && win[0] <= WINDOW_EPSILON && win[1] >= 1 - WINDOW_EPSILON;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Returns true when a window actually existed AND was narrower than the
|
|
367
|
+
* full domain (and was cleared), so the caller can leave Escape alone
|
|
368
|
+
* when the host needs it — a full-domain window has nothing to reset. */
|
|
355
369
|
function resetWindow(): boolean {
|
|
370
|
+
if (isFullDomain(activeWindow.value)) return false;
|
|
356
371
|
if (!commitIfChanged(null)) return false;
|
|
357
372
|
// A reset that interrupts a drag (Escape mid-brush) must drop the
|
|
358
373
|
// gesture too, or the next pointermove re-brushes from the stale
|
|
@@ -818,7 +833,10 @@ export default defineComponent({
|
|
|
818
833
|
focus; this sheet only pins the seat (position) and a
|
|
819
834
|
surface backing via its override vars. The glyph slots
|
|
820
835
|
as a lucide component (the house-normal integration:
|
|
821
|
-
every in-tree HkIconButton slots its glyph).
|
|
836
|
+
every in-tree HkIconButton slots its glyph). A
|
|
837
|
+
full-domain window has nothing to reset, so the chip
|
|
838
|
+
only renders for a REAL sub-domain selection. */}
|
|
839
|
+
{win && !isFullDomain(win) && (
|
|
822
840
|
<HIconButton
|
|
823
841
|
class="hk-range-ribbon-reset"
|
|
824
842
|
size={24}
|
|
@@ -829,6 +847,7 @@ export default defineComponent({
|
|
|
829
847
|
>
|
|
830
848
|
{{ icon: () => <RotateCcw aria-hidden="true" /> }}
|
|
831
849
|
</HIconButton>
|
|
850
|
+
)}
|
|
832
851
|
</>
|
|
833
852
|
)}
|
|
834
853
|
</div>
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
height: var(--hk-ride-content, auto);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
.hk-ride[data-hk-ride-ready].hk-ride--from > .hk-ride-chrome-start,
|
|
80
|
+
.hk-ride[data-hk-ride-ready].hk-ride--to > .hk-ride-chrome-start {
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
.hk-ride[data-hk-ride-ready].hk-ride--from > .hk-ride-chrome-end,
|
|
80
85
|
.hk-ride[data-hk-ride-ready].hk-ride--to > .hk-ride-chrome-end {
|
|
81
86
|
position: absolute;
|
|
@@ -70,6 +70,14 @@ export interface HeightRide {
|
|
|
70
70
|
* ride needs); calling it mid-ride re-measures the live, compressed
|
|
71
71
|
* box and is therefore a no-op unless `force` is passed. */
|
|
72
72
|
refresh(force?: boolean): boolean;
|
|
73
|
+
/** Inline style vars for the CURRENT cache, or null when nothing has
|
|
74
|
+
* been measured yet. Consumers bind this in their render so the very
|
|
75
|
+
* first painted frame of a remount already carries the ride (a post-
|
|
76
|
+
* mount publish is one paint too late: the enter-from frame would show
|
|
77
|
+
* a full-height frame with unscaled content — read as "clipping"). */
|
|
78
|
+
styleVars(): Record<string, string> | null;
|
|
79
|
+
/** True when a measured split exists (drives the ready attribute). */
|
|
80
|
+
hasCache(): boolean;
|
|
73
81
|
/** Re-apply the LAST measured split without touching the DOM boxes.
|
|
74
82
|
* A surface that unmounts its frame on close loses the inline variables
|
|
75
83
|
* with it; republishing on the next mount is what makes the very first
|
|
@@ -138,15 +146,12 @@ export function useHeightRide(
|
|
|
138
146
|
if (child === c || !(child instanceof HTMLElement)) continue;
|
|
139
147
|
chromeNatural += child.offsetHeight;
|
|
140
148
|
}
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
? c.offsetHeight
|
|
148
|
-
: Math.max(0, f.offsetHeight - chromeNatural);
|
|
149
|
-
const measured = contentNatural > 0 ? contentNatural : c.offsetHeight;
|
|
149
|
+
// The content region's own layout box is the ONLY honest source. With
|
|
150
|
+
// no ride class the body is flex-natural; while a ride class is present
|
|
151
|
+
// the box is locked to the published var (stable, no new information —
|
|
152
|
+
// and the frame's height is the ride's own output, so deriving the
|
|
153
|
+
// content from it would feed every early error forward forever).
|
|
154
|
+
const measured = c.offsetHeight;
|
|
150
155
|
if (measured <= 0) return false;
|
|
151
156
|
contentH = measured;
|
|
152
157
|
chrome = chromeNatural;
|
|
@@ -172,8 +177,19 @@ export function useHeightRide(
|
|
|
172
177
|
return true;
|
|
173
178
|
};
|
|
174
179
|
|
|
180
|
+
const styleVars = (): Record<string, string> | null =>
|
|
181
|
+
contentH <= 0
|
|
182
|
+
? null
|
|
183
|
+
: {
|
|
184
|
+
"--hk-ride-chrome": `${chrome}px`,
|
|
185
|
+
"--hk-ride-content": `${contentH}px`,
|
|
186
|
+
"--hk-ride-factor": String(factorOf()),
|
|
187
|
+
};
|
|
188
|
+
|
|
175
189
|
return {
|
|
176
190
|
refresh,
|
|
191
|
+
styleVars,
|
|
192
|
+
hasCache: () => contentH > 0,
|
|
177
193
|
publish,
|
|
178
194
|
chromeHeight: () => chrome,
|
|
179
195
|
contentHeight: () => contentH,
|