@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.305",
3
+ "version": "0.55.306",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -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 label column",
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
  };