@celestia-island/hikari 0.55.150 → 0.55.151

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.150",
3
+ "version": "0.55.151",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -96,6 +96,15 @@ export default defineComponent({
96
96
  /** Whether cards can be dragged between lanes. */
97
97
  draggable: { type: Boolean, default: false },
98
98
  ariaLabel: { type: String, default: undefined },
99
+ /** Forwarded to the strip's HkScrollContainer: spend the shell-
100
+ * published --hk-scroll-tail-bottom as an in-flow tail spacer so a
101
+ * dock floating over the board never covers its last row at the
102
+ * scroll end (see HkScrollContainer's dockTailClearance). Only
103
+ * effective on axis="vertical"/"both" boards: the default
104
+ * horizontal axis carries its tail on the cross axis, where the
105
+ * contract generates no spacer and the shrink pin is inert in the
106
+ * block viewport — a horizontal board gets a no-op by design. */
107
+ dockTailClearance: { type: Boolean, default: false },
99
108
  },
100
109
  emits: ["move"],
101
110
  setup(props, { slots, emit, expose }) {
@@ -191,6 +200,7 @@ export default defineComponent({
191
200
  class="hk-kanban-strip"
192
201
  axis={stripAxis.value}
193
202
  overscanScreens={1}
203
+ dockTailClearance={props.dockTailClearance}
194
204
  >
195
205
  <div
196
206
  class="hk-kanban-lanes"
@@ -8,11 +8,20 @@
8
8
  // host so consumers size their content end-padding against the dock
9
9
  // without hand-rolled px. The dock-edge fade rides on data-dock-fade.
10
10
  //
11
+ // The FLOATING-dock twin (dockTailClearance) is contract-tested below:
12
+ // it spends the shell-published --hk-scroll-tail-bottom as an in-flow
13
+ // tail spacer instead of reserving layout space.
14
+ //
11
15
  // House style: raw createApp mounts on shared containers (no
12
16
  // @vue/test-utils).
17
+ import { readFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
13
21
  import { afterEach, describe, expect, it } from "vitest";
14
22
  import { createApp, h, defineComponent } from "vue";
15
23
 
24
+ import HkKanban from "./HkKanban";
16
25
  import HkScrollContainer from "./HkScrollContainer";
17
26
 
18
27
  const mounts: ReturnType<typeof createApp>[] = [];
@@ -90,3 +99,83 @@ describe("HkScrollContainer dock slots", () => {
90
99
  expect(host.style.getPropertyValue("--hk-scroll-dock-bottom")).toBe("");
91
100
  });
92
101
  });
102
+
103
+ describe("HkScrollContainer floating-dock tail clearance", () => {
104
+ function mountKanban(props: Record<string, unknown> = {}) {
105
+ const container = document.createElement("div");
106
+ document.body.appendChild(container);
107
+ containers.push(container);
108
+ const app = createApp(defineComponent({
109
+ setup() {
110
+ return () => h(HkKanban, {
111
+ lanes: [{ id: "a" }, { id: "b" }],
112
+ laneKeyOf: (lane: unknown) => (lane as { id: string }).id,
113
+ cardsOf: () => [{ id: "c" }],
114
+ axis: "vertical",
115
+ ...props,
116
+ }, { card: () => h("div", { class: "card" }, "card") });
117
+ },
118
+ }));
119
+ mounts.push(app);
120
+ return app.mount(container);
121
+ }
122
+
123
+ it("marks the host only while dockTailClearance is on", () => {
124
+ const on = mountDock({ default: () => h("div", "rows") }, { dockTailClearance: true });
125
+ expect((on.$el as HTMLElement).getAttribute("data-dock-tail")).toBe("true");
126
+ const off = mountDock({ default: () => h("div", "rows") });
127
+ expect((off.$el as HTMLElement).getAttribute("data-dock-tail")).toBeNull();
128
+ });
129
+
130
+ it("forwards through HkKanban onto the strip host", () => {
131
+ const vm = mountKanban({ dockTailClearance: true });
132
+ const strip = (vm.$el as HTMLElement).querySelector(".hk-kanban-strip")!;
133
+ expect(strip.getAttribute("data-dock-tail")).toBe("true");
134
+ const bare = mountKanban();
135
+ const bareStrip = (bare.$el as HTMLElement).querySelector(".hk-kanban-strip")!;
136
+ expect(bareStrip.getAttribute("data-dock-tail")).toBeNull();
137
+ });
138
+
139
+ it("spends the published var on a shrink-safe in-flow spacer (style contract)", () => {
140
+ // happy-dom has no layout, so the geometry half of the contract is
141
+ // pinned as a style contract: the spacer trails the viewport's own
142
+ // content as a `flex: none; display: block` ::after (a column-flex
143
+ // viewport cannot compress it; the explicit display is what makes
144
+ // its height apply in the DEFAULT block viewport, where a bare
145
+ // ::after is an inline box and `height` is inert on it — measured in
146
+ // a real engine: zero clearance without, content+spacer with), and
147
+ // EVERY direct child of the viewport is pinned against main-axis
148
+ // shrinking while the prop is on. The pin is the other load-bearing
149
+ // half: a child that behaves as a shrinkable flex item once the
150
+ // viewport is flex-ified (this library's own `.hk-kanban-lanes` ships
151
+ // only `min-height: 100%`, so the flex-item initial `0 1 auto`
152
+ // applies) collapses to the viewport height and overflows its rows
153
+ // visibly, painting them OVER a spacer that trails the box — zero
154
+ // reachable clearance on any content taller than the viewport while
155
+ // short content measures green (the defect that kept a consumer
156
+ // board's tail under its dock through three padding rounds,
157
+ // root-caused in a real layout engine 2026-10-12). The spacer is
158
+ // additionally gated OFF axis="horizontal" containers: the tail is
159
+ // the cross axis there, and an ungated block spacer leaks onto the
160
+ // hidden vertical axis (measured: scrollHeight +spacer, and a
161
+ // programmatic scrollTop shifts content up behind
162
+ // overflow-y:hidden with no wheel path back).
163
+ const scss = readFileSync(
164
+ join(dirname(fileURLToPath(import.meta.url)), "HkScrollContainer.scss"),
165
+ "utf8",
166
+ );
167
+ const spacer = scss.match(
168
+ /\.hk-scroll-container\[data-dock-tail="true"\]:not\(\[data-axis="horizontal"\]\) > \.hk-scroll-container-viewport::after\s*\{[^}]*\}/,
169
+ );
170
+ expect(spacer, "tail spacer rule must exist").not.toBeNull();
171
+ expect(spacer![0]).toMatch(/content:\s*""/);
172
+ expect(spacer![0]).toMatch(/display:\s*block/);
173
+ expect(spacer![0]).toMatch(/flex:\s*none/);
174
+ expect(spacer![0]).toMatch(/height:\s*var\(--hk-scroll-tail-bottom,\s*0px\)/);
175
+ const pin = scss.match(
176
+ /\.hk-scroll-container\[data-dock-tail="true"\] > \.hk-scroll-container-viewport > \*\s*\{[^}]*\}/,
177
+ );
178
+ expect(pin, "viewport-child shrink pin must exist").not.toBeNull();
179
+ expect(pin![0]).toMatch(/flex-shrink:\s*0/);
180
+ });
181
+ });
@@ -84,6 +84,56 @@
84
84
  mask-image: linear-gradient(to bottom, transparent, #000 var(--hk-scroll-dock-fade, 2rem));
85
85
  }
86
86
 
87
+ /* ── Floating-dock tail clearance (dockTailClearance opt-in) ──────
88
+ A dock that floats OVER the scroll area (shell-level fixed plane —
89
+ NOT a dockTop/dockBottom slot) covers the viewport's tail. The shell
90
+ publishes its live measured occupancy as --hk-scroll-tail-bottom;
91
+ this spends it as an IN-FLOW tail spacer on the viewport's own
92
+ content, so the room is reachable scroll range that only appears at
93
+ the scroll end (mid-scroll content keeps flowing under the dock).
94
+
95
+ Two halves, both load-bearing (root-caused 2026-10-12 in a real
96
+ layout engine after three consumer-side padding rounds "never
97
+ entered the flow"):
98
+ 1. the spacer is a `flex: none` ::after WITH `display: block` — a
99
+ column-flex viewport cannot compress it (and blockifies it
100
+ anyway), while in the DEFAULT block viewport the explicit
101
+ display is what makes its height apply at all: a bare ::after
102
+ there is a non-replaced INLINE box and `height` is inert on it
103
+ (measured: block viewport, 300px child, 50px spacer ->
104
+ scrollHeight 300 without the display, 350 with);
105
+ 2. every direct child of the viewport is pinned against main-axis
106
+ SHRINKING while the prop is on — a child that behaves as a
107
+ shrinkable flex item once the viewport is flex-ified
108
+ (`.hk-kanban-lanes` ships only `min-height: 100%`, so the
109
+ flex-item initial `0 1 auto` applies) collapses to the viewport
110
+ height and overflows its rows VISIBLY, painting them over a
111
+ spacer that trails the box: zero reachable clearance on any
112
+ content taller than the viewport, while short content (demo
113
+ boards, empty-ish pages) measures green and hides the defect.
114
+ The spacer is inert while the var is unset/0px; the shrink pin is
115
+ active whenever the prop is on (at S=0 it changes only box extent,
116
+ not scroll range). The spacer is NOT generated on axis="horizontal"
117
+ containers: there the tail is the cross axis, and an ungated block
118
+ spacer stacks below the full-height content on the HIDDEN vertical
119
+ axis (measured: scrollHeight +50, and a programmatic scrollTop
120
+ shifts the content up behind overflow-y:hidden with no wheel path
121
+ back). The pin stays ungated (flex-shrink is inert on block children
122
+ and a no-op on the align-center aligner, which is already
123
+ flex:none). Consumer note: a higher-specificity consumer ::after
124
+ recipe on the viewport overrides this spacer's height — adopting
125
+ the prop means deleting the hand-rolled recipe, not stacking it. */
126
+ .hk-scroll-container[data-dock-tail="true"] > .hk-scroll-container-viewport > * {
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .hk-scroll-container[data-dock-tail="true"]:not([data-axis="horizontal"]) > .hk-scroll-container-viewport::after {
131
+ content: "";
132
+ display: block;
133
+ flex: none;
134
+ height: var(--hk-scroll-tail-bottom, 0px);
135
+ }
136
+
87
137
 
88
138
  .hk-scroll-container[data-fade="true"][data-h-overflow="start"] > .hk-scroll-container-viewport {
89
139
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--hk-scroll-fade-size));
@@ -85,6 +85,52 @@ export default defineComponent({
85
85
  * scroll content dissolving at the dock's edge reads as
86
86
  * intentional instead of a hard clip. */
87
87
  dockFade: { type: Boolean, default: true },
88
+ /** Opt-in tail clearance for FLOATING docks (2026-10-12): a dock that
89
+ * lives OUTSIDE this container (a shell-level fixed plane — a page's
90
+ * bottom-center dock bar, e.g. chest's DockBar) covers the bottom of
91
+ * whatever scroll area it hovers over. The shell publishes the dock's
92
+ * live measured occupancy as `--hk-scroll-tail-bottom` (on any
93
+ * ancestor, typically :root); with this prop the container spends
94
+ * that var as an IN-FLOW tail spacer on the viewport's own content,
95
+ * so the room becomes reachable scroll range that only appears once
96
+ * the scroll end is reached — mid-scroll content keeps flowing under
97
+ * the dock across the full viewport. The published value is the
98
+ * whole tail room the publisher wants spent (dock occupancy plus
99
+ * whatever breathing room its design rules add).
100
+ *
101
+ * The spacer is a `flex: none; display: block` ::after, and every
102
+ * direct child of the viewport is pinned against main-axis SHRINKING
103
+ * while the prop is on. Both halves are load-bearing: a child that
104
+ * behaves as a shrinkable flex item once the viewport is flex-ified
105
+ * (hikari's own `.hk-kanban-lanes` ships only `min-height: 100%`, so
106
+ * the flex-item initial `0 1 auto` applies) overflows its own box
107
+ * visibly and paints OVER a spacer that trails the box — the exact
108
+ * defect that kept a consumer board's tail under its dock through
109
+ * three rounds of padding adjustments that "never entered the flow"
110
+ * (chest follow-up to #1538/#1542, root-caused and measured in a real
111
+ * layout engine, 2026-10-12); and the explicit `display: block` is
112
+ * what makes the spacer's height apply in the DEFAULT block viewport
113
+ * at all (a bare ::after there is an inline box and `height` is inert
114
+ * on it — measured: zero clearance without, content+spacer with).
115
+ * The spacer is inert while the var is unset/0px, so dockless pages
116
+ * keep their full centering box; the shrink pin is active whenever
117
+ * the prop is on. Unlike the dockTop/dockBottom slots (layout-space
118
+ * docks that shrink the viewport permanently), this contract is for
119
+ * docks that float OVER the scroll area by design. Do not combine it
120
+ * with a `--hk-scroll-dock-bottom` end-padding recipe for the SAME
121
+ * dock (double clearance). The spacer is not generated on
122
+ * axis="horizontal" containers — the tail is the cross axis there,
123
+ * and an ungated block spacer leaks onto the hidden vertical axis
124
+ * (measured: scrollHeight grows and a programmatic scrollTop shifts
125
+ * content up behind overflow-y:hidden) — while the shrink pin still
126
+ * applies. A higher-specificity consumer ::after recipe on the
127
+ * viewport overrides this spacer's height: adopting the prop means
128
+ * deleting the hand-rolled recipe, not stacking it. The shrink pin's
129
+ * accepted cost: a child that legitimately needs main-axis shrink
130
+ * inside an opted-in container (a nested pane meant to fit the
131
+ * viewport and scroll internally) cannot shrink; consumers escape
132
+ * with a >=(0,2,0) override loaded after the library CSS. */
133
+ dockTailClearance: { type: Boolean, default: false },
88
134
  /** Back-to-top signal (scroll parameter every scrollable template
89
135
  * can hand to a floating quick-action pad): while set, the
90
136
  * container derives a visibility signal from its own throttled
@@ -539,6 +585,7 @@ export default defineComponent({
539
585
  data-axis={props.axis}
540
586
  data-align={alignCenter() ? "center" : undefined}
541
587
  data-fade={props.fade ? "true" : undefined}
588
+ data-dock-tail={props.dockTailClearance ? "true" : undefined}
542
589
  data-dock-fade={
543
590
  props.dockFade && dockBottomSlot
544
591
  ? "bottom"