@celestia-island/hikari 0.55.337 → 0.55.338

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.337",
3
+ "version": "0.55.338",
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",
@@ -37,9 +37,15 @@
37
37
 
38
38
  Ride only, by the root hook HkKanban emits for a ride host: a pin board
39
39
  must keep the full-height lanes box it always had, so this rule never
40
- reaches an existing board. Specificity (0,3,0) beats the base rule
41
- above; a host that wants its own rule can override it the same way. */
42
- .hk-kanban[data-dock-mode="ride"] .hk-kanban-lanes {
40
+ reaches an existing board. DIRECT-CHILD chain (2026-11-10 follow-up,
41
+ same shape as chest's consumer-side override): the earlier descendant
42
+ form ALSO released the lanes box of a board NESTED inside a card — an
43
+ inner `.hk-kanban` is not the ride host and must keep its own
44
+ full-height lanes (its strip viewport scrolls its own cards; the
45
+ release would cut its lane chrome short). Specificity (0,5,0) beats the
46
+ base rule above; a host that wants its own rule can override it the
47
+ same way. */
48
+ .hk-kanban[data-dock-mode="ride"] > .hk-kanban-strip > .hk-scroll-container-viewport > .hk-kanban-lanes {
43
49
  min-height: 0;
44
50
  }
45
51
 
@@ -122,8 +122,9 @@ export default defineComponent({
122
122
  * window's leftover (measured on a real board: 275.8px static vs the
123
123
  * 139px dock, i.e. zero improvement over the pin board). The
124
124
  * attribute is emitted only while a ride dock is genuinely mounted (not
125
- * for pin, not for a horizontal board, not for an empty slot), so every
126
- * existing board is untouched. */
125
+ * for pin, not for a horizontal board, not for an empty board — with no
126
+ * lanes the whole strip including the dock is not rendered — and not
127
+ * for an empty dock slot), so every existing board is untouched. */
127
128
  dockMode: { type: String as PropType<"pin" | "ride">, default: "pin" },
128
129
  },
129
130
  emits: ["move"],
@@ -219,8 +220,17 @@ export default defineComponent({
219
220
  // always had, so no existing board changes. Gated on the dock actually
220
221
  // being mounted too (S2-2): a requested-but-empty slot is legal, and
221
222
  // releasing the lanes box for it would only shrink lane chrome on a
222
- // short board while no dock is there to justify it.
223
- const rideBoard = docksOnTail && props.dockMode === "ride" && !!slots.dockBottom;
223
+ // short board while no dock is there to justify it. Gated on a
224
+ // non-empty board as well (2026-11-10 follow-up): with
225
+ // `lanes.length === 0` the whole strip is replaced by the empty slot,
226
+ // so the container — and any dock in it — is not rendered at all; the
227
+ // hook means "a ride dock is mounted", and on an empty board none
228
+ // ever is.
229
+ const rideBoard =
230
+ docksOnTail &&
231
+ props.dockMode === "ride" &&
232
+ !!slots.dockBottom &&
233
+ props.lanes.length > 0;
224
234
  return (
225
235
  <div
226
236
  class="hk-kanban"
@@ -163,10 +163,15 @@ describe("HkScrollContainer floating-dock tail clearance", () => {
163
163
  // hidden vertical axis (measured: scrollHeight +spacer, and a
164
164
  // programmatic scrollTop shifts content up behind
165
165
  // overflow-y:hidden with no wheel path back).
166
+ // Comments stripped (block + line, house two-step form — see
167
+ // HkScrollContainer.dockRide.test.ts): a rule commented out must go
168
+ // red, not hide in the text the regex matches.
166
169
  const scss = readFileSync(
167
170
  join(dirname(fileURLToPath(import.meta.url)), "HkScrollContainer.scss"),
168
171
  "utf8",
169
- );
172
+ )
173
+ .replace(/\/\*[\s\S]*?\*\//g, "")
174
+ .replace(/^[ \t]*\/\/.*$/gm, "");
170
175
  const spacer = scss.match(
171
176
  /\.hk-scroll-container\[data-dock-tail="true"\]:not\(\[data-axis="horizontal"\]\) > \.hk-scroll-container-viewport::after\s*\{[^}]*\}/,
172
177
  );
@@ -164,6 +164,25 @@ function stubGeometry(
164
164
  Object.defineProperties(el, desc);
165
165
  }
166
166
 
167
+ /** Stub one element's drawn vertical band (house pattern,
168
+ * HkScrollContainer.selectionHint.test.ts): the selection hint reads
169
+ * `getBoundingClientRect` on the viewport AND on each selected row, and
170
+ * happy-dom reports zeros for both. */
171
+ function stubBand(el: HTMLElement, top: number, bottom: number) {
172
+ const rect = {
173
+ x: 0,
174
+ y: top,
175
+ top,
176
+ bottom,
177
+ left: 0,
178
+ right: 100,
179
+ width: 100,
180
+ height: bottom - top,
181
+ toJSON: () => ({}),
182
+ };
183
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue(rect as DOMRect);
184
+ }
185
+
167
186
  /** The physical edges the shared <HkEdgeGlow> is painting right now. */
168
187
  function bandsOf(host: HTMLElement): (string | null)[] {
169
188
  return Array.from(host.querySelectorAll<HTMLElement>(".hk-edge-glow-band"), (band) =>
@@ -231,17 +250,30 @@ const MOUNT_SLOTS = {
231
250
  dockBottom: () => h("div", { class: "the-dock" }, "dock"),
232
251
  };
233
252
 
253
+ /** The sheets with comments STRIPPED (block + line comments, house
254
+ * two-step form, tokenInventory.test.ts): every source-contract match
255
+ * below judges the LIVE rules. A rule commented out must go red — a bare
256
+ * regex happily matches commented text, which is exactly how the
257
+ * 2026-11-10 mutation round's comment-out survivor stayed green (and how
258
+ * the round-1 review of a73a9f99 caught the same hole re-created by the
259
+ * newer assertions). Both steps are needed: the sheets carry block
260
+ * comments as well as line comments. */
234
261
  const scss = readFileSync(
235
262
  join(dirname(fileURLToPath(import.meta.url)), "HkScrollContainer.scss"),
236
263
  "utf8",
237
- );
264
+ )
265
+ .replace(/\/\*[\s\S]*?\*\//g, "")
266
+ .replace(/^[ \t]*\/\/.*$/gm, "");
238
267
 
239
268
  /** The board's own sheet — the ride board's content-height switch lives
240
- * there (see the S0 block in the ride contract tests). */
269
+ * there (see the S0 block in the ride contract tests). Stripped like
270
+ * `scss` above, for the same reason. */
241
271
  const kanbanScss = readFileSync(
242
272
  join(dirname(fileURLToPath(import.meta.url)), "HkKanban.scss"),
243
273
  "utf8",
244
- );
274
+ )
275
+ .replace(/\/\*[\s\S]*?\*\//g, "")
276
+ .replace(/^[ \t]*\/\/.*$/gm, "");
245
277
 
246
278
  afterEach(() => {
247
279
  restoreRect?.();
@@ -351,6 +383,46 @@ describe("HkScrollContainer ride-mode bottom dock", () => {
351
383
  expect(host.getAttribute("data-dock-mode")).toBeNull();
352
384
  });
353
385
 
386
+ it("zeroes the published heights when a pin dock slot disappears at runtime", async () => {
387
+ // 2026-11-10 follow-up: the onUpdated re-point path is
388
+ // placement-agnostic and the ride case above guards it — this pin
389
+ // sibling pins the SAME contract there, where the published vars feed
390
+ // consumer end-padding instead of ride geometry: a dock removed at
391
+ // runtime must republish the inert 0px on BOTH vars, else a consumer
392
+ // keeps padding clear of a dock that no longer exists.
393
+ stubMeasuredHeight(".hk-scroll-dock", 139);
394
+ const container = document.createElement("div");
395
+ document.body.appendChild(container);
396
+ containers.push(container);
397
+ const show = ref(true);
398
+ const app = createApp(defineComponent({
399
+ setup() {
400
+ return () => {
401
+ const slots: Record<string, () => VNodeChild> = {
402
+ default: () => h("div", { class: "content" }, "rows"),
403
+ };
404
+ if (show.value) slots.dockBottom = () => h("div", { class: "the-dock" }, "dock");
405
+ // dockMode unset — the pin placement, the pre-ride default.
406
+ return h(HkScrollContainer, {}, slots);
407
+ };
408
+ },
409
+ }));
410
+ mounts.push(app);
411
+ const host = (app.mount(container) as unknown as { $el: HTMLElement }).$el;
412
+ expect(host.querySelector(".hk-scroll-dock[data-side='bottom']")).toBeTruthy();
413
+ expect(host.style.getPropertyValue("--hk-scroll-dock-bottom")).toBe("139px");
414
+
415
+ show.value = false;
416
+ await nextTick();
417
+ expect(host.querySelector(".hk-scroll-dock")).toBeNull();
418
+ expect(host.style.getPropertyValue("--hk-scroll-dock-bottom")).toBe("0px");
419
+ expect(host.style.getPropertyValue("--hk-scroll-dock-top")).toBe("0px");
420
+ // The pin container keeps its pre-ride attribute set: no ride hook and,
421
+ // with the slot gone, no dock-edge fade either.
422
+ expect(host.getAttribute("data-dock-mode")).toBeNull();
423
+ expect(host.getAttribute("data-dock-fade")).toBeNull();
424
+ });
425
+
354
426
  it("drops the bottom glow band under a mounted ride dock (pin keeps it)", async () => {
355
427
  // S2-1: the shared <HkEdgeGlow> painter lives on the container root at
356
428
  // z-index 2, i.e. ABOVE the viewport — with a ride dock inside that
@@ -375,6 +447,38 @@ describe("HkScrollContainer ride-mode bottom dock", () => {
375
447
  expect(bandsOf(pin.host)).toEqual(["top", "bottom"]);
376
448
  });
377
449
 
450
+ it("drops the bottom band from the selection hint under a mounted ride dock (pin keeps it)", async () => {
451
+ // 2026-11-10 follow-up: the selection hint's painter rides the SAME
452
+ // `painterEdges` filter as the overflow glow (same function, same
453
+ // bottom drop — the band would paint over the ride dock's lower limb
454
+ // exactly like the glow's). Until now only the glow call site had a
455
+ // test; the review's mutation g (bypassing the filter at the hint call
456
+ // site) survived the whole suite green. The verdict attribute — the
457
+ // documented consumer contract — is untouched: only the BAND is
458
+ // dropped.
459
+ const slots = {
460
+ default: () => h("div", { class: "is-active" }, "row"),
461
+ dockBottom: () => h("div", { class: "the-dock" }, "dock"),
462
+ };
463
+ const ride = mountWithApi(slots, { dockMode: "ride", selectionHint: true });
464
+ const rideViewport = ride.host.querySelector<HTMLElement>(".hk-scroll-container-viewport")!;
465
+ stubBand(rideViewport, 0, 400);
466
+ stubBand(rideViewport.querySelector<HTMLElement>(".is-active")!, 500, 560);
467
+ ride.api.value!.refresh();
468
+ await nextTick();
469
+ expect(ride.host.getAttribute("data-selection-beyond")).toBe("bottom");
470
+ expect(bandsOf(ride.host)).toEqual([]);
471
+
472
+ const pin = mountWithApi(slots, { selectionHint: true });
473
+ const pinViewport = pin.host.querySelector<HTMLElement>(".hk-scroll-container-viewport")!;
474
+ stubBand(pinViewport, 0, 400);
475
+ stubBand(pinViewport.querySelector<HTMLElement>(".is-active")!, 500, 560);
476
+ pin.api.value!.refresh();
477
+ await nextTick();
478
+ expect(pin.host.getAttribute("data-selection-beyond")).toBe("bottom");
479
+ expect(bandsOf(pin.host)).toEqual(["bottom"]);
480
+ });
481
+
378
482
  it("warns once in dev when autoFollow is combined with a ride dock", async () => {
379
483
  // S2-3: the combination reproduces the full-height-content trap that
380
484
  // S0 fixed (autoFollow bottom-anchors a min-height:100% box), so it is
@@ -405,6 +509,25 @@ describe("HkScrollContainer ride-mode bottom dock", () => {
405
509
  expect((pin.$el as HTMLElement).getAttribute("data-dock-fade")).toBe("bottom");
406
510
  });
407
511
 
512
+ it("publishes the dock-edge fade for a top dock even in ride mode", () => {
513
+ // 2026-11-10 follow-up: the ride suppression concerns the BOTTOM dock
514
+ // only — that is the one living inside the masked viewport band. A
515
+ // `dockTop` dock is not on that edge, so the container publishes the
516
+ // dock-edge fade for it as usual, flipping the attribute to "top"
517
+ // (and, with no bottom slot, emitting no ride hook at all).
518
+ const vm = mountDock(
519
+ {
520
+ default: () => h("div", { class: "content" }, "rows"),
521
+ dockTop: () => h("div", { class: "top-chrome" }, "top"),
522
+ },
523
+ { dockMode: "ride" },
524
+ );
525
+ const host = vm.$el as HTMLElement;
526
+ expect(host.getAttribute("data-dock-fade")).toBe("top");
527
+ expect(host.getAttribute("data-dock-mode")).toBeNull();
528
+ expect(host.querySelector(".hk-scroll-dock-ride")).toBeNull();
529
+ });
530
+
408
531
  it("rides when the axis is both", () => {
409
532
  const vm = mountDock(MOUNT_SLOTS, { dockMode: "ride", axis: "both" });
410
533
  const host = vm.$el as HTMLElement;
@@ -443,7 +566,7 @@ describe("HkScrollContainer ride-mode bottom dock", () => {
443
566
  expect(host.getAttribute("data-dock-mode")).toBeNull();
444
567
  });
445
568
 
446
- it("leaves the pin placement byte-compatible", () => {
569
+ it("leaves the pin placement structurally compatible", () => {
447
570
  // dockMode is additive: the default must keep today's DOM exactly —
448
571
  // sibling dock, no spacer, no ride class, dock fade published, both
449
572
  // measured-height vars on the host.
@@ -501,6 +624,10 @@ describe("HkScrollContainer ride placement style contract", () => {
501
624
  // lands it UNDER the dock. `scroll-padding-bottom` belongs on the
502
625
  // scrollport (the viewport) and is exactly this band; keyed on the ride
503
626
  // hook, which is emitted only while the dock is mounted.
627
+ //
628
+ // `scss` is the comment-stripped sheet (see the module const): the
629
+ // 2026-11-10 mutation check found that commenting the rule out left
630
+ // this test green — the regex happily matched the commented text.
504
631
  const rule = scss.match(
505
632
  /\.hk-scroll-container\[data-dock-mode="ride"\] > \.hk-scroll-container-viewport \{[^}]*\}/,
506
633
  );
@@ -510,6 +637,40 @@ describe("HkScrollContainer ride placement style contract", () => {
510
637
  );
511
638
  });
512
639
 
640
+ it("suppresses the dockTailClearance spacer while a ride dock is mounted", () => {
641
+ // 2026-11-10 follow-up S3 (flagged by two review rounds): with
642
+ // `dockTailClearance` on AND a ride dock mounted, the tail spacer's
643
+ // `::after` lined up BEHIND the ride dock in the content flow — two
644
+ // stacked tail reservations for one dock. Ride wins: the sheet drops
645
+ // the spacer box under the same two root hooks.
646
+ //
647
+ // happy-dom computes no CSS, so this is pinned the suite's usual way —
648
+ // the runtime precondition (both hooks on the same root) plus the rule
649
+ // itself as a source contract.
650
+ const vm = mountDock(MOUNT_SLOTS, { dockMode: "ride", dockTailClearance: true });
651
+ const host = vm.$el as HTMLElement;
652
+ expect(host.getAttribute("data-dock-mode")).toBe("ride");
653
+ expect(host.getAttribute("data-dock-tail")).toBe("true");
654
+
655
+ const generation = scss.match(
656
+ /\.hk-scroll-container\[data-dock-tail="true"\]:not\(\[data-axis="horizontal"\]\) > \.hk-scroll-container-viewport::after \{[^}]*\}/,
657
+ );
658
+ expect(generation, "the tail spacer generation rule exists").not.toBeNull();
659
+ const guard = scss.match(
660
+ /\.hk-scroll-container\[data-dock-mode="ride"\]\[data-dock-tail\] > \.hk-scroll-container-viewport::after \{[^}]*\}/,
661
+ );
662
+ expect(guard, "ride suppresses the tail spacer").not.toBeNull();
663
+ // `content: none` removes the box outright — a zero-height box could
664
+ // still perturb the layout.
665
+ expect(guard![0]).toMatch(/content:\s*none/);
666
+ // Equal specificity (0,4,0) with the generation rule, so source order
667
+ // is what makes the suppression win.
668
+ expect(
669
+ scss.indexOf(guard![0]),
670
+ "the suppression wins on source order, not on specificity",
671
+ ).toBeGreaterThan(scss.indexOf(generation![0]));
672
+ });
673
+
513
674
  it("zeroes the container's own bottom fade band in ride mode", () => {
514
675
  // S2-c: the ride dock lives INSIDE the viewport at its trailing edge,
515
676
  // so the container's own fade mask dissolves the dock's lower limb
@@ -717,6 +878,24 @@ describe("HkKanban ride-dock forwarding", () => {
717
878
  expect(vm.$el.querySelector(".hk-scroll-dock-ride")).toBeNull();
718
879
  });
719
880
 
881
+ it("emits no ride hook for an empty board (the strip is not mounted at all)", () => {
882
+ // 2026-11-10 follow-up: with `lanes.length === 0` the whole strip —
883
+ // container, dock slot and all — is replaced by the empty slot, so no
884
+ // ride dock is ever mounted and the root must keep its pre-ride
885
+ // attribute set. The hook means "a ride dock is mounted", and here
886
+ // none is: the previously ungated emission contradicted the prop doc.
887
+ const vm = mountKanban(
888
+ { axis: "vertical", dockMode: "ride", lanes: [] },
889
+ {
890
+ dockBottom: () => h("div", { class: "board-dock" }, "dock"),
891
+ empty: () => h("div", { class: "board-empty" }, "empty"),
892
+ },
893
+ );
894
+ expect(vm.$el.getAttribute("data-dock-mode")).toBeNull();
895
+ expect(vm.$el.querySelector(".hk-kanban-strip")).toBeNull();
896
+ expect(vm.$el.querySelector(".board-empty")).toBeTruthy();
897
+ });
898
+
720
899
  it("ignores the slot on a horizontal board (the cross-axis tail)", () => {
721
900
  // The default axis: pre-2026-11-10 shape — no forwarded dock slot and
722
901
  // no ride chrome, because a horizontal strip's tail is its cross axis.
@@ -734,7 +913,7 @@ describe("HkKanban ride-dock forwarding", () => {
734
913
  });
735
914
 
736
915
  describe("HkKanban ride board content-height contract", () => {
737
- it("releases the full-height lanes box under the ride hook only", () => {
916
+ it("releases the full-height lanes box under the ride hook only, via the direct-child chain", () => {
738
917
  // S0, measured on a real board: in a column-flex strip viewport
739
918
  // `min-height: 100%` makes the lanes BOX as tall as the viewport, so
740
919
  // the in-flow spacer + sticky dock always sit below the fold — the dock
@@ -742,11 +921,21 @@ describe("HkKanban ride board content-height contract", () => {
742
921
  // window's leftover (275.8px static / 563.8px scrolled against a 139px
743
922
  // dock). Releasing it is the half of the contract that makes the air
744
923
  // follow the CONTENT end.
924
+ //
925
+ // 2026-11-10 follow-up: the release selector is the DIRECT-CHILD chain
926
+ // (same shape as chest's consumer-side override). The earlier
927
+ // descendant form also released the lanes box of a board NESTED inside
928
+ // a card of the outer ride board — the inner board is not the ride
929
+ // host, and its strip viewport scrolls its own cards, so cutting its
930
+ // lanes short broke its lane chrome.
745
931
  const release = kanbanScss.match(
746
- /\.hk-kanban\[data-dock-mode="ride"\] \.hk-kanban-lanes \{[^}]*\}/,
932
+ /\.hk-kanban\[data-dock-mode="ride"\] > \.hk-kanban-strip > \.hk-scroll-container-viewport > \.hk-kanban-lanes \{[^}]*\}/,
747
933
  );
748
934
  expect(release, "the ride boards release their lanes min-height").not.toBeNull();
749
935
  expect(release![0]).toMatch(/min-height:\s*0/);
936
+ // And the descendant spelling stays gone: reintroducing it (the
937
+ // mutation) would leak the release into nested boards again.
938
+ expect(kanbanScss).not.toMatch(/\.hk-kanban\[data-dock-mode="ride"\] \.hk-kanban-lanes/);
750
939
  // The base rule keeps the fill for every other mode (pin, horizontal
751
940
  // boards): a short board still reaches the strip's floor there.
752
941
  const base = kanbanScss.match(/\.hk-kanban-lanes \{[^}]*\}/);
@@ -342,6 +342,14 @@
342
342
  geometry is unchanged by this line (measured). */
343
343
  inset-inline-start: 0;
344
344
  flex: none;
345
+ /* WEAK STACKING GUARANTEE (2026-11-10 follow-up): 1 stays a literal, not
346
+ * a `--hk-scroll-dock-ride-z` variable. It only lifts the dock above the
347
+ * PLAIN in-flow content scrolling under it; a descendant of the viewport
348
+ * that is itself positioned with z-index >= 2 (a floating toolbar, a drag
349
+ * layer) paints OVER the pinned dock. No shipped consumer stacks one
350
+ * inside the dock band, and a variable would add an override surface
351
+ * before a second consumer needs it (additive-chrome discipline) —
352
+ * promote it to a variable if one appears. */
345
353
  z-index: 1;
346
354
  /* Transparent band: the dock surface inside opts back in (see the
347
355
  * pointer contract above). */
@@ -429,6 +437,27 @@
429
437
  height: var(--hk-scroll-tail-bottom, 0px);
430
438
  }
431
439
 
440
+ /* ── Ride wins over the tail spacer (mutual exclusion) ─────────────
441
+ 2026-11-10 follow-up S3, flagged by two review rounds: with
442
+ `dockTailClearance` on AND a ride dock mounted, the spacer above lined
443
+ up BEHIND the ride dock (in flow after it) and the tail got TWO
444
+ stacked reservations — the shell-published `--hk-scroll-tail-bottom`
445
+ band plus the dock's own measured box. The two props answer the same
446
+ question for two different dock geometries: the clearance spacer is
447
+ for a dock FLOATING OVER the scroll area from outside the flow, ride
448
+ mounts the dock INSIDE it and spends its measured box as the air. So
449
+ while a ride dock is actually mounted (the root carries BOTH
450
+ `data-dock-mode="ride"` and `data-dock-tail`) the spacer box is not
451
+ generated at all. Same specificity (0,4,0) as the generation rule
452
+ above and declared after it, so source order decides; `content: none`
453
+ removes the box outright rather than shrinking it (a zero-height box
454
+ could still perturb the layout). The shrink pin above stays: it is
455
+ harmless under ride and still load-bearing while the prop serves a
456
+ floating dock (no ride hook present). */
457
+ .hk-scroll-container[data-dock-mode="ride"][data-dock-tail] > .hk-scroll-container-viewport::after {
458
+ content: none;
459
+ }
460
+
432
461
 
433
462
  /* ── Selection edge hint (selectionHint opt-in) ────────────────────
434
463
  A list taller than its viewport hides its SELECTED row as soon as it
@@ -281,7 +281,19 @@ export default defineComponent({
281
281
  * accepted cost: a child that legitimately needs main-axis shrink
282
282
  * inside an opted-in container (a nested pane meant to fit the
283
283
  * viewport and scroll internally) cannot shrink; consumers escape
284
- * with a >=(0,2,0) override loaded after the library CSS. */
284
+ * with a >=(0,2,0) override loaded after the library CSS.
285
+ *
286
+ * MUTUALLY EXCLUSIVE WITH `dockMode="ride"` (2026-11-10 follow-up):
287
+ * the two props answer the same tail question for two different dock
288
+ * geometries — this spacer reserves a band for a dock FLOATING OVER
289
+ * the scroll area from outside the flow, ride mounts the dock inside
290
+ * it and spends its measured box as the air. With both armed and a
291
+ * ride dock actually mounted, the tail spacer is NOT generated (the
292
+ * sheet drops the viewport's `::after` under
293
+ * `[data-dock-mode="ride"][data-dock-tail]`) — ride wins, the two
294
+ * reservations would stack into a double-clear band. The shrink pin
295
+ * above stays: harmless under ride, still load-bearing while this
296
+ * prop serves a floating dock. */
285
297
  dockTailClearance: { type: Boolean, default: false },
286
298
  /** Where a mounted `dockBottom` dock LIVES (2026-11-10 user ruling):
287
299
  *
@@ -326,7 +338,8 @@ export default defineComponent({
326
338
  * its only effect (measured: identical to the pin board's 184px,
327
339
  * and worse at the scroll end). HkKanban releases its own
328
340
  * `.hk-kanban-lanes` box for a ride host
329
- * (`.hk-kanban[data-dock-mode="ride"] .hk-kanban-lanes
341
+ * (`.hk-kanban[data-dock-mode="ride"] > .hk-kanban-strip >
342
+ * .hk-scroll-container-viewport > .hk-kanban-lanes
330
343
  * { min-height: 0 }`); a custom host has to do the same.
331
344
  * - `autoFollow` IS NOT SUPPORTED TOGETHER WITH RIDE (documented,
332
345
  * not policed beyond a dev warning): autoFollow bottom-anchors a
@@ -381,6 +394,13 @@ export default defineComponent({
381
394
  * SLOT CONTENT THAT MUST CATCH EVENTS HAS TO DECLARE
382
395
  * `pointer-events: auto` ON THE PART THAT CATCHES THEM — the
383
396
  * wrapper does not hand interactivity to a generic slot child.
397
+ * - MUTUALLY EXCLUSIVE WITH `dockTailClearance` (2026-11-10
398
+ * follow-up): that prop reserves a tail band for a dock FLOATING
399
+ * OVER the scroll area from outside the flow; ride mounts the
400
+ * dock inside it. With both set and a ride dock mounted, the
401
+ * tail spacer is not generated (the sheet drops the viewport's
402
+ * `::after` under the same two root hooks) and ride wins — the
403
+ * stacked reservations would double-clear the same tail.
384
404
  *
385
405
  * Ignored (falls back to `pin`) on `axis="horizontal"` containers:
386
406
  * there the content tail is the cross axis and an in-flow block
@@ -1372,7 +1392,7 @@ export default defineComponent({
1372
1392
  // owns the viewport's bottom edge — so the sheet must keep fading that
1373
1393
  // edge and HkKanban must keep its full-height lanes box (S2-2).
1374
1394
  const rideDockMounted = !!dockBottomSlot && rideBottom;
1375
- if (import.meta.env.DEV && !warnedAutoFollowRide && rideDockMounted && props.autoFollow) {
1395
+ if (import.meta.env?.DEV && !warnedAutoFollowRide && rideDockMounted && props.autoFollow) {
1376
1396
  warnedAutoFollowRide = true;
1377
1397
  // eslint-disable-next-line no-console -- dev-only unsupported-combination alarm
1378
1398
  console.warn(