@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
|
@@ -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"
|