@celestia-island/hikari 0.55.305 → 0.55.306
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
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
// ONE label column for both surfaces: the overview paints through an
|
|
24
24
|
// empty gutter of this width, so an x% bar sits at the same inline
|
|
25
25
|
// position in the strip and in its lane (the strip locates segments).
|
|
26
|
+
// Wave (2026-11-06 raw-records five): the label column is JS-MEASURED —
|
|
27
|
+
// the component publishes the widest rendered label as a px value on the
|
|
28
|
+
// root (two-phase free/measure/pin). THIS fixed column is the no-JS /
|
|
29
|
+
// pre-measure fallback: a shared width keeps strip-lane x% alignment
|
|
30
|
+
// before the measurement lands (a bare max-content here would resolve
|
|
31
|
+
// 0px in the empty gutter and misalign everything).
|
|
26
32
|
--hk-range-ribbon-label-col: minmax(48px, 84px);
|
|
27
33
|
|
|
28
34
|
display: flex;
|
|
@@ -42,7 +48,9 @@
|
|
|
42
48
|
min-width: 0;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// The empty first cell that lines the strip up with the lane tracks.
|
|
51
|
+
// The empty first cell that lines the strip up with the lane tracks. Its
|
|
52
|
+
// width comes from the shared var, which the component JS-measures and
|
|
53
|
+
// publishes on the root (two-phase free/measure/pin).
|
|
46
54
|
.hk-range-ribbon-overview-gutter {
|
|
47
55
|
display: block;
|
|
48
56
|
min-width: 0;
|
|
@@ -55,6 +63,29 @@
|
|
|
55
63
|
min-width: 0;
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
.hk-range-ribbon[data-minimal] {
|
|
67
|
+
gap: 0;
|
|
68
|
+
|
|
69
|
+
/* Bare-bar layout: single column, gutter probe out of the way — the
|
|
70
|
+
strip spans the full width (its in-flow content is absolute, so
|
|
71
|
+
without this the max-content column would collapse to zero). */
|
|
72
|
+
.hk-range-ribbon-overview {
|
|
73
|
+
grid-template-columns: minmax(0, 1fr);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.hk-range-ribbon-overview-gutter {
|
|
77
|
+
display: none;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.hk-range-ribbon[data-minimal] .hk-range-ribbon-strip {
|
|
82
|
+
// Minimal mode (raw-records five): the track reads through muted-based
|
|
83
|
+
// tokens — the surface-based track read whitish inside dark phone
|
|
84
|
+
// modals. The inset hairline keeps the frame visible but quiet.
|
|
85
|
+
background: rgb(var(--color-muted) / 25%);
|
|
86
|
+
box-shadow: inset 0 0 0 1px rgb(var(--color-border) / 40%);
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
.hk-range-ribbon-strip {
|
|
59
90
|
position: relative;
|
|
60
91
|
display: block;
|
|
@@ -1466,7 +1466,7 @@ describe("HkRangeRibbon stylesheet contract", () => {
|
|
|
1466
1466
|
it("shares one label column between the overview and the lanes", () => {
|
|
1467
1467
|
expect(
|
|
1468
1468
|
declaration(ruleBody(css, ".hk-range-ribbon"), "--hk-range-ribbon-label-col"),
|
|
1469
|
-
"the root declares the shared
|
|
1469
|
+
"the root declares the shared fallback column (JS publishes the measured px on top)",
|
|
1470
1470
|
).toBe("minmax(48px, 84px)");
|
|
1471
1471
|
const overviewColumns = declaration(
|
|
1472
1472
|
ruleBody(css, ".hk-range-ribbon-overview"),
|
|
@@ -1494,4 +1494,57 @@ describe("HkRangeRibbon stylesheet contract", () => {
|
|
|
1494
1494
|
expect(declaration(endAnchor, "right")).toBe("0");
|
|
1495
1495
|
expect(declaration(endAnchor, "transform")).toBe("none");
|
|
1496
1496
|
});
|
|
1497
|
+
|
|
1498
|
+
// Wave (2026-11-06 raw-records five): MINIMAL MODE - the bare dual-handle
|
|
1499
|
+
// bar. Contract: data-minimal on the root, strip present + windowed at
|
|
1500
|
+
// the full domain, ZERO lane chrome (no lanes block, no lane rows, no
|
|
1501
|
+
// overview minimap, no gutter probe), and the standard mode keeps its
|
|
1502
|
+
// lane chrome + alignment probe.
|
|
1503
|
+
it("minimal mode renders the bare dual-handle bar (no lane chrome)", () => {
|
|
1504
|
+
const root = mountRibbon({ minimal: true });
|
|
1505
|
+
expect(root.getAttribute("data-minimal")).toBe("true");
|
|
1506
|
+
const stripEl = root.querySelector(".hk-range-ribbon-strip")!;
|
|
1507
|
+
expect(stripEl).not.toBeNull();
|
|
1508
|
+
// The strip carries the slider a11y (role/tabindex/label/valuenow).
|
|
1509
|
+
expect(stripEl.getAttribute("aria-label")).toBe("Observation range");
|
|
1510
|
+
// Uncontrolled default window = the FULL domain: valuenow sits at 0
|
|
1511
|
+
// (domain start) and the valuetext reads the whole range.
|
|
1512
|
+
expect(stripEl.getAttribute("aria-valuenow")).toBe("0");
|
|
1513
|
+
expect(stripEl.getAttribute("aria-valuetext")).toContain("0%");
|
|
1514
|
+
// The lanes BLOCK is skipped entirely (no lane rows at all).
|
|
1515
|
+
expect(root.querySelector(".hk-range-ribbon-lanes")).toBeNull();
|
|
1516
|
+
expect(root.querySelector(".hk-range-ribbon-lane")).toBeNull();
|
|
1517
|
+
// The overview minimap container shells remain but render EMPTY, and
|
|
1518
|
+
// the gutter probe is display:none (visibility is the semantic).
|
|
1519
|
+
const minimap = root.querySelector(".hk-range-ribbon-overview-lanes");
|
|
1520
|
+
expect(minimap!.children.length).toBe(0);
|
|
1521
|
+
// (The gutter probe's display:none in minimal mode is enforced by the
|
|
1522
|
+
// static scss assertion at the end of this file — happy-dom cannot
|
|
1523
|
+
// resolve the nested attribute selector.)
|
|
1524
|
+
expect(root.querySelector(".hk-range-ribbon-overview-segment")).toBeNull();
|
|
1525
|
+
// Uncontrolled default window = the FULL domain (edge to edge).
|
|
1526
|
+
// Standard consumers keep the lane chrome + the alignment probe.
|
|
1527
|
+
const std = mountRibbon({});
|
|
1528
|
+
expect(std.querySelector(".hk-range-ribbon-lane")).not.toBeNull();
|
|
1529
|
+
expect(std.querySelector(".hk-range-ribbon-overview-gutter")).not.toBeNull();
|
|
1530
|
+
// Default LANES vocabulary (cpu/boot/parse/io/write/flush).
|
|
1531
|
+
expect(std.textContent).toContain("CPU");
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// Static scss contract: in minimal mode the gutter probe is display:none
|
|
1535
|
+
// and the overview collapses to a single column (the strip spans the
|
|
1536
|
+
// full width).
|
|
1537
|
+
it("minimal mode overrides the overview grid in the sheet", () => {
|
|
1538
|
+
const scss = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "HkRangeRibbon.scss"), "utf-8");
|
|
1539
|
+
// The nested scss compiles to a single-column overview + a hidden
|
|
1540
|
+
// gutter probe in minimal mode; assert the SOURCE structure (the flat
|
|
1541
|
+
// compiled selector does not appear verbatim in the nested sheet).
|
|
1542
|
+
expect(scss).toContain(".hk-range-ribbon[data-minimal] {");
|
|
1543
|
+
expect(scss).toMatch(
|
|
1544
|
+
/[\s\S]*?\.hk-range-ribbon\[data-minimal\] \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\)/,
|
|
1545
|
+
);
|
|
1546
|
+
expect(scss).toMatch(
|
|
1547
|
+
/\.hk-range-ribbon\[data-minimal\] \{[\s\S]*?\.hk-range-ribbon-overview-gutter \{[\s\S]*?display: none/,
|
|
1548
|
+
);
|
|
1549
|
+
});
|
|
1497
1550
|
});
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { RotateCcw } from "lucide-vue-next";
|
|
2
|
-
import {
|
|
3
|
-
computed,
|
|
4
|
-
defineComponent,
|
|
5
|
-
onBeforeUnmount,
|
|
6
|
-
onMounted,
|
|
7
|
-
ref,
|
|
8
|
-
useId,
|
|
9
|
-
watch,
|
|
10
|
-
type PropType,
|
|
11
|
-
} from "vue";
|
|
2
|
+
import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref, type PropType, useId, watch } from "vue";
|
|
12
3
|
|
|
13
4
|
import { useI18n } from "../i18n/context";
|
|
14
5
|
import HIconButton from "./HkIconButton";
|
|
@@ -269,6 +260,14 @@ export default defineComponent({
|
|
|
269
260
|
windowEnabled: { type: Boolean, default: true },
|
|
270
261
|
/** Narrowest window the ribbon will produce, in domain fractions. */
|
|
271
262
|
minWindow: { type: Number, default: MIN_WINDOW_FALLBACK },
|
|
263
|
+
/** Wave (2026-11-06 raw-records five): MINIMAL MODE — a bare dual-handle
|
|
264
|
+
* range bar. No lane rows, no overview minimap, no label gutter; the
|
|
265
|
+
* strip spans the full width and its track uses muted-based tokens
|
|
266
|
+
* (the surface-based track read whitish inside dark phone modals).
|
|
267
|
+
* The uncontrolled default window is already the FULL domain, so a
|
|
268
|
+
* minimal bar starts pinned edge to edge. `segments` / `selectedId`
|
|
269
|
+
* are IGNORED in this mode (no segment DOM to select from). */
|
|
270
|
+
minimal: { type: Boolean, default: false },
|
|
272
271
|
},
|
|
273
272
|
emits: {
|
|
274
273
|
select: (_id: string) => true,
|
|
@@ -277,6 +276,7 @@ export default defineComponent({
|
|
|
277
276
|
setup(props, { emit }) {
|
|
278
277
|
const { t } = useI18n();
|
|
279
278
|
const stripRef = ref<HTMLElement | null>(null);
|
|
279
|
+
|
|
280
280
|
const internalWindow = ref<WindowRange | null>(null);
|
|
281
281
|
/** Id of the segment whose tip card is floating (hover or focus). */
|
|
282
282
|
const tipSegmentId = ref<string | null>(null);
|
|
@@ -548,6 +548,53 @@ export default defineComponent({
|
|
|
548
548
|
if (tipSegmentId.value === id) tipSegmentId.value = null;
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
// Wave (raw-records five): the label column is JS-MEASURED. The fixed
|
|
552
|
+
// minmax(48px,84px) floor left a dead band next to short labels, and a
|
|
553
|
+
// CSS max-content cannot align two independent grids (each lane sizes
|
|
554
|
+
// to its own label; the overview gutter has no text to size from — a
|
|
555
|
+
// hidden-probe attempt measured misaligned in real Chrome). Measuring
|
|
556
|
+
// the widest rendered label and publishing it as the shared var gives
|
|
557
|
+
// BOTH surfaces the same resolved column: adaptive and aligned.
|
|
558
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
559
|
+
const pinnedCol = ref<string | null>(null);
|
|
560
|
+
/** TWO-PHASE measure: FREE the column first. A pinned px width clips
|
|
561
|
+
* long labels, and a clipped label's offsetWidth equals the clip - so
|
|
562
|
+
* a naive offsetWidth max can never GROW after the first pin (and a
|
|
563
|
+
* smaller max leaves the dead band). Freeing lets labels reflow to
|
|
564
|
+
* natural width; the next frame reads the true text width
|
|
565
|
+
* (scrollWidth == offsetWidth when unclipped) and re-pins only when
|
|
566
|
+
* the value changed (converges - no ResizeObserver loop). */
|
|
567
|
+
const syncLabelCol = () => {
|
|
568
|
+
const root = rootRef.value;
|
|
569
|
+
if (!root) return;
|
|
570
|
+
root.style.setProperty("--hk-range-ribbon-label-col", "max-content");
|
|
571
|
+
requestAnimationFrame(() => {
|
|
572
|
+
const r = rootRef.value;
|
|
573
|
+
if (!r) return;
|
|
574
|
+
const labels = r.querySelectorAll<HTMLElement>(".hk-range-ribbon-lane-label");
|
|
575
|
+
let max = 0;
|
|
576
|
+
for (const el of labels) {
|
|
577
|
+
max = Math.max(max, el.offsetWidth, el.scrollWidth);
|
|
578
|
+
}
|
|
579
|
+
if (max <= 0) return;
|
|
580
|
+
const next = `${max}px`;
|
|
581
|
+
// Guard on the CURRENT INLINE value, not the last measurement:
|
|
582
|
+
// after the FREE phase the inline value is "max-content", and an
|
|
583
|
+
// unchanged measurement must STILL re-pin (the re-pin restores the
|
|
584
|
+
// px column the free took away - skipping it because "40px == 40px"
|
|
585
|
+
// left the bar resting misaligned at freed max-content, measured in
|
|
586
|
+
// real Chrome: field 20 vs tracks 41/60).
|
|
587
|
+
if (r.style.getPropertyValue("--hk-range-ribbon-label-col") !== next) {
|
|
588
|
+
pinnedCol.value = next;
|
|
589
|
+
r.style.setProperty("--hk-range-ribbon-label-col", next);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
const labelObserver =
|
|
594
|
+
typeof ResizeObserver !== "undefined"
|
|
595
|
+
? new ResizeObserver(() => syncLabelCol())
|
|
596
|
+
: null;
|
|
597
|
+
|
|
551
598
|
onMounted(() => {
|
|
552
599
|
// Window-level listeners: a pointer captured by a drag target still
|
|
553
600
|
// emits moves/ups here, and a drag that leaves the strip keeps
|
|
@@ -556,9 +603,22 @@ export default defineComponent({
|
|
|
556
603
|
window.addEventListener("pointermove", onPointerMove);
|
|
557
604
|
window.addEventListener("pointerup", endGesture);
|
|
558
605
|
window.addEventListener("pointercancel", endGesture);
|
|
606
|
+
// Publish the measured label column (see syncLabelCol above): after
|
|
607
|
+
// the first paint, on any lane-label resize (locale swap, viewport
|
|
608
|
+
// font change), and when the lanes vocabulary itself changes.
|
|
609
|
+
syncLabelCol();
|
|
610
|
+
labelObserver?.observe(rootRef.value!);
|
|
611
|
+
watch(
|
|
612
|
+
lanes,
|
|
613
|
+
() => {
|
|
614
|
+
nextTick(syncLabelCol);
|
|
615
|
+
},
|
|
616
|
+
{ deep: true },
|
|
617
|
+
);
|
|
559
618
|
});
|
|
560
619
|
|
|
561
620
|
onBeforeUnmount(() => {
|
|
621
|
+
labelObserver?.disconnect();
|
|
562
622
|
window.removeEventListener("pointermove", onPointerMove);
|
|
563
623
|
window.removeEventListener("pointerup", endGesture);
|
|
564
624
|
window.removeEventListener("pointercancel", endGesture);
|
|
@@ -657,7 +717,7 @@ export default defineComponent({
|
|
|
657
717
|
{/* The overview paints the FULL domain only — it never follows
|
|
658
718
|
the zoom, so the window's position stays locatable. */}
|
|
659
719
|
<div class="hk-range-ribbon-overview-lanes" aria-hidden="true">
|
|
660
|
-
{lanes.value.map((lane) => (
|
|
720
|
+
{!props.minimal && lanes.value.map((lane) => (
|
|
661
721
|
<div class="hk-range-ribbon-overview-row" key={lane.key} data-lane={lane.key}>
|
|
662
722
|
{(byLane.get(lane.key) ?? []).map((placed) => {
|
|
663
723
|
const box = fullDomainBox(placed);
|
|
@@ -701,7 +761,9 @@ export default defineComponent({
|
|
|
701
761
|
|
|
702
762
|
return (
|
|
703
763
|
<div
|
|
764
|
+
ref={rootRef}
|
|
704
765
|
class="hk-range-ribbon"
|
|
766
|
+
data-minimal={props.minimal || undefined}
|
|
705
767
|
data-windowed={win ? "true" : undefined}
|
|
706
768
|
data-window-enabled={props.windowEnabled ? "true" : "false"}
|
|
707
769
|
onKeydown={onRootKeyDown}
|
|
@@ -710,6 +772,11 @@ export default defineComponent({
|
|
|
710
772
|
(empty gutter first), so x% means the same inline position on
|
|
711
773
|
both surfaces — the strip can be used to locate a bar. */}
|
|
712
774
|
<div class="hk-range-ribbon-overview">
|
|
775
|
+
{/* The gutter's width IS the shared label column: the
|
|
776
|
+
component JS-measures the widest rendered label and
|
|
777
|
+
publishes it on the root (two-phase free/measure/pin), so
|
|
778
|
+
this empty cell keeps the strip's x% aligned with the
|
|
779
|
+
lanes' x% at any label width. */}
|
|
713
780
|
<span class="hk-range-ribbon-overview-gutter" aria-hidden="true" />
|
|
714
781
|
<div class="hk-range-ribbon-overview-field">
|
|
715
782
|
{strip}
|
|
@@ -780,6 +847,7 @@ export default defineComponent({
|
|
|
780
847
|
</span>
|
|
781
848
|
)}
|
|
782
849
|
</div>
|
|
850
|
+
{!props.minimal && (
|
|
783
851
|
<div class="hk-range-ribbon-lanes">
|
|
784
852
|
{lanes.value.map((lane) => (
|
|
785
853
|
<div class="hk-range-ribbon-lane" key={lane.key} data-lane={lane.key}>
|
|
@@ -795,6 +863,7 @@ export default defineComponent({
|
|
|
795
863
|
</div>
|
|
796
864
|
))}
|
|
797
865
|
</div>
|
|
866
|
+
)}
|
|
798
867
|
</div>
|
|
799
868
|
);
|
|
800
869
|
};
|