@celestia-island/hikari 0.55.324 → 0.55.326
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 +1 -1
- package/src/components/HkAdminShell.test.tsx +11 -5
- package/src/components/HkAdminTablePage.tsx +67 -41
- package/src/components/HkCardList.tsx +13 -13
- package/src/components/HkFaceShell.test.tsx +53 -2
- package/src/components/HkFaceShell.tsx +37 -13
- package/src/components/HkScrollContainer.test.ts +46 -1
- package/src/components/HkScrollContainer.tsx +18 -208
- package/src/components/HkScrollProgressDock.scss +101 -0
- package/src/components/HkScrollProgressDock.test.tsx +1069 -0
- package/src/components/HkScrollProgressDock.tsx +532 -0
- package/src/components/HkTable.sections.test.tsx +1 -1
- package/src/components/HkTable.tsx +10 -9
- package/src/i18n/locales/ar/components.json +2 -1
- package/src/i18n/locales/de/components.json +2 -1
- package/src/i18n/locales/en/components.json +2 -1
- package/src/i18n/locales/es/components.json +2 -1
- package/src/i18n/locales/fr/components.json +2 -1
- package/src/i18n/locales/ja/components.json +2 -1
- package/src/i18n/locales/ko/components.json +2 -1
- package/src/i18n/locales/pt/components.json +2 -1
- package/src/i18n/locales/ru/components.json +2 -1
- package/src/i18n/locales/zh-Hans/components.json +2 -1
- package/src/i18n/locales/zh-Hant/components.json +2 -1
- package/src/index.ts +9 -6
- package/src/components/HkScrollProgress.scss +0 -61
- package/src/components/HkScrollProgress.test.tsx +0 -687
- package/src/components/HkScrollProgress.tsx +0 -103
package/package.json
CHANGED
|
@@ -81,7 +81,14 @@ const setWidth = (w: number) => {
|
|
|
81
81
|
function paddingInner(container: HTMLElement): HTMLElement | null | undefined {
|
|
82
82
|
const viewport = container.querySelector(".hk-scroll-container-viewport")
|
|
83
83
|
?? container.querySelector(".hk-scroll-container");
|
|
84
|
-
|
|
84
|
+
// The shell's fallback scroll-progress dock (2026-11-09) wraps the
|
|
85
|
+
// content with a display:contents scope — descend through it, the
|
|
86
|
+
// padding wrapper is still the viewport's only layout box.
|
|
87
|
+
let inner = viewport?.firstElementChild as HTMLElement | null | undefined;
|
|
88
|
+
if (inner?.classList.contains("hk-scroll-progress-dock-scope")) {
|
|
89
|
+
inner = inner.firstElementChild as HTMLElement;
|
|
90
|
+
}
|
|
91
|
+
return inner;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
function mobileInnerPadding(container: HTMLElement): string {
|
|
@@ -240,10 +247,9 @@ describe("HkAdminShell", () => {
|
|
|
240
247
|
{ header: () => null, sidebar: NAV, content: CONTENT },
|
|
241
248
|
));
|
|
242
249
|
// The padded inner wrapper exists INSIDE the scroll viewport (not as
|
|
243
|
-
// padding on the container itself)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const inner = viewport?.firstElementChild as HTMLElement | null | undefined;
|
|
250
|
+
// padding on the container itself) — behind the fallback dock's
|
|
251
|
+
// display:contents scope (see paddingInner).
|
|
252
|
+
const inner = paddingInner(c);
|
|
247
253
|
expect(inner).toBeTruthy();
|
|
248
254
|
expect(inner?.style.padding).toBe("2rem");
|
|
249
255
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineComponent, type Component, type PropType } from "vue";
|
|
2
2
|
import { HAlert, HCard, HEmptyState, HSpinner, HTable, useI18n } from "@celestia-island/hikari";
|
|
3
3
|
import HkCrossfade from "./HkCrossfade";
|
|
4
|
+
import HkScrollProgressDock from "./HkScrollProgressDock";
|
|
4
5
|
import { HkPageHeader } from "./HkPageHeader";
|
|
5
6
|
import type { HkRowManageGate } from "./HkTable";
|
|
6
7
|
|
|
@@ -80,6 +81,24 @@ export const HkAdminTablePage = defineComponent({
|
|
|
80
81
|
* zero so a fresh or empty list shows no "共 0 条" flash. Pages
|
|
81
82
|
* that render their own count elsewhere set this false. */
|
|
82
83
|
countReadout: { type: Boolean, default: true },
|
|
84
|
+
/** The page's own scroll-progress DOCK (user direction 2026-11-09:
|
|
85
|
+
* the list large-component uses the dock itself). DEFAULT ON — the
|
|
86
|
+
* page mounts an HkScrollProgressDock around the body crossfade so
|
|
87
|
+
* the table (or a body-slot roster) self-registers against it and
|
|
88
|
+
* the pill floats over the scrolling viewport; the crossfade's
|
|
89
|
+
* generation swaps happen INSIDE the dock, so the registration
|
|
90
|
+
* survives every exchange. A page under a shell fallback dock has
|
|
91
|
+
* its OWN dock stand down automatically (the nested-dock rule
|
|
92
|
+
* serves the outermost enabled dock — the fallback keeps the
|
|
93
|
+
* pill); set `false` to demote the page — lists then fall through
|
|
94
|
+
* to the nearest enabled dock above, if any. Layering: the shell
|
|
95
|
+
* master switch (HkFaceShell `scrollProgress`) > this prop > the
|
|
96
|
+
* dock's own `disabled`. Like every dock gate, the layering is
|
|
97
|
+
* RESOLVED
|
|
98
|
+
* ONCE at the dock's setup — hosts pass these statically, and a
|
|
99
|
+
* runtime flip needs a remount (the HkScrollContainer prop
|
|
100
|
+
* precedent). */
|
|
101
|
+
progressDock: { type: Boolean, default: true },
|
|
83
102
|
},
|
|
84
103
|
emits: {
|
|
85
104
|
reorder: (_fromIndex: number, _toIndex: number) => true,
|
|
@@ -151,48 +170,55 @@ export const HkAdminTablePage = defineComponent({
|
|
|
151
170
|
every host dissolves between them instead of popping. The
|
|
152
171
|
key collapses loading-vs-empty into one "placeholder"
|
|
153
172
|
generation: spinner → empty is a data arrival, not a slot
|
|
154
|
-
identity change worth animating.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<HCard padded={false}>
|
|
179
|
-
<HTable
|
|
180
|
-
columns={props.columns}
|
|
181
|
-
rows={props.rows}
|
|
182
|
-
rowKey={props.rowKey}
|
|
183
|
-
total={props.total}
|
|
184
|
-
draggable={props.draggable}
|
|
185
|
-
manageable={props.manageable}
|
|
186
|
-
onReorder={(from: number, to: number) => emit("reorder", from, to)}
|
|
187
|
-
onManage={(row: Record<string, unknown>, opener: HTMLElement) =>
|
|
188
|
-
emit("manage", row, opener)
|
|
189
|
-
}
|
|
173
|
+
identity change worth animating.
|
|
174
|
+
The whole exchange rides INSIDE this page's scroll-progress
|
|
175
|
+
dock (default on; `progressDock:false` demotes to a bare
|
|
176
|
+
passthrough): the dock must wrap the CROSSFADE, not sit in
|
|
177
|
+
a generation — a generation-scoped dock would unmount with
|
|
178
|
+
every swap and drop the registration. */}
|
|
179
|
+
<HkScrollProgressDock disabled={!props.progressDock}>
|
|
180
|
+
<HkCrossfade
|
|
181
|
+
swapKey={
|
|
182
|
+
props.error ? "error"
|
|
183
|
+
: props.loading && !props.rows.length ? "placeholder"
|
|
184
|
+
: !props.rows.length ? "placeholder"
|
|
185
|
+
: "content"
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
{props.error ? (
|
|
189
|
+
<HAlert message={props.error} />
|
|
190
|
+
) : props.loading && !props.rows.length ? (
|
|
191
|
+
<HSpinner center />
|
|
192
|
+
) : !props.rows.length ? (
|
|
193
|
+
<HEmptyState
|
|
194
|
+
boxed
|
|
195
|
+
title={emptyTitle}
|
|
196
|
+
description={props.emptyDescription}
|
|
190
197
|
>
|
|
191
|
-
{
|
|
192
|
-
</
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
{emptySlots}
|
|
199
|
+
</HEmptyState>
|
|
200
|
+
) : slots.body ? (
|
|
201
|
+
<div class="hk-admin-table-page-body">{slots.body()}</div>
|
|
202
|
+
) : (
|
|
203
|
+
<HCard padded={false}>
|
|
204
|
+
<HTable
|
|
205
|
+
columns={props.columns}
|
|
206
|
+
rows={props.rows}
|
|
207
|
+
rowKey={props.rowKey}
|
|
208
|
+
total={props.total}
|
|
209
|
+
draggable={props.draggable}
|
|
210
|
+
manageable={props.manageable}
|
|
211
|
+
onReorder={(from: number, to: number) => emit("reorder", from, to)}
|
|
212
|
+
onManage={(row: Record<string, unknown>, opener: HTMLElement) =>
|
|
213
|
+
emit("manage", row, opener)
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
{slots}
|
|
217
|
+
</HTable>
|
|
218
|
+
</HCard>
|
|
219
|
+
)}
|
|
220
|
+
</HkCrossfade>
|
|
221
|
+
</HkScrollProgressDock>
|
|
196
222
|
{slots["create-modal"]?.()}
|
|
197
223
|
{slots["edit-modal"]?.()}
|
|
198
224
|
</div>
|
|
@@ -9,7 +9,7 @@ import HkTooltip from "./HkTooltip";
|
|
|
9
9
|
import { hasRenderedContent } from "../runtime/hasRenderedContent";
|
|
10
10
|
import { useMediaQuery } from "../runtime/useMediaQuery";
|
|
11
11
|
import type { StatTone } from "./HkStatCard";
|
|
12
|
-
import { HK_SCROLL_PROGRESS_REGISTRAR, type HkScrollProgressHandle } from "./
|
|
12
|
+
import { HK_SCROLL_PROGRESS_REGISTRAR, type HkScrollProgressHandle } from "./HkScrollProgressDock";
|
|
13
13
|
import "./HkCardList.scss";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -47,22 +47,22 @@ export const HkCardList = defineComponent({
|
|
|
47
47
|
props: {
|
|
48
48
|
/** Authoritative row total the scroll-progress pill uses as its
|
|
49
49
|
* denominator (the server count behind a windowed/infinite list).
|
|
50
|
-
* Forwarded to the nearest enclosing
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* Forwarded to the nearest enclosing enabled HkScrollProgressDock,
|
|
51
|
+
* exactly like HkAdminTablePage's `total`. Omitted: the pill falls
|
|
52
|
+
* back to counting the delivered DOM rows (right for
|
|
53
|
+
* fully-delivered lists, drifting low for windowed ones — pass the
|
|
54
|
+
* server count there). */
|
|
55
55
|
total: { type: Number, default: undefined },
|
|
56
56
|
},
|
|
57
57
|
setup(props, { slots }) {
|
|
58
58
|
// Scroll-progress registration (the HkTable precedent): the NEAREST
|
|
59
|
-
// enclosing
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
59
|
+
// enclosing ENABLED HkScrollProgressDock provides the registrar —
|
|
60
|
+
// provide/inject scoping resolves the shell viewport for console
|
|
61
|
+
// pages and keeps a list registered against an inner dock out of
|
|
62
|
+
// the shell. The handle hands the pill the list element as the row
|
|
63
|
+
// root, the `total` prop live, and the rendered HkListRow elements
|
|
64
|
+
// as the rows (a card list's rows ARE its data rows — no
|
|
65
|
+
// section-band caveat).
|
|
66
66
|
const listRef = ref<HTMLElement | null>(null);
|
|
67
67
|
const progressRegistrar = inject(HK_SCROLL_PROGRESS_REGISTRAR, null);
|
|
68
68
|
const progressHandle: HkScrollProgressHandle = {
|
|
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { createApp, h } from "vue";
|
|
5
|
+
import { createApp, defineComponent, h, inject } from "vue";
|
|
6
6
|
|
|
7
7
|
// HDrawer teleports and animates; replace it with an inline passthrough
|
|
8
8
|
// (same stub shape as HkAdminShell.test.tsx) so the shell contract is
|
|
@@ -36,6 +36,7 @@ vi.mock("@celestia-island/hikari", async (importOriginal) => {
|
|
|
36
36
|
|
|
37
37
|
import { HkFaceShell } from "./HkFaceShell";
|
|
38
38
|
import { HkAdminShell } from "./HkAdminShell";
|
|
39
|
+
import { HK_SCROLL_PROGRESS_ALLOWED } from "./HkScrollProgressDock";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* HkFaceShell — the generalized face shell extracted from HkAdminShell
|
|
@@ -96,6 +97,11 @@ describe("HkFaceShell stage mode", () => {
|
|
|
96
97
|
// The document host is GONE — that is the stage contract.
|
|
97
98
|
expect(c.querySelector(".hk-scroll-container")).toBeNull();
|
|
98
99
|
expect(c.querySelector(".hk-scroll-container-viewport")).toBeNull();
|
|
100
|
+
// …and so is the fallback scroll-progress dock (document-mode only —
|
|
101
|
+
// a stage face owns its own geometry, so the shell mounts no dock
|
|
102
|
+
// scope and provides no pill chrome at all).
|
|
103
|
+
expect(c.querySelector(".hk-scroll-progress-dock-scope")).toBeNull();
|
|
104
|
+
expect(c.querySelector(".hk-scroll-progress-dock")).toBeNull();
|
|
99
105
|
// No padding wrapper anywhere: the content slot's first element IS
|
|
100
106
|
// the main region's child (contentPadding is a document-mode prop).
|
|
101
107
|
expect(main!.firstElementChild?.textContent).toBe("CONTENT");
|
|
@@ -158,12 +164,57 @@ describe("HkFaceShell document mode (the default)", () => {
|
|
|
158
164
|
expect(c.querySelector(".hk-scroll-container")).toBeTruthy();
|
|
159
165
|
const viewport = c.querySelector(".hk-scroll-container-viewport")
|
|
160
166
|
?? c.querySelector(".hk-scroll-container");
|
|
161
|
-
|
|
167
|
+
// The shell's fallback scroll-progress dock wraps the content
|
|
168
|
+
// (2026-11-09): its scope is display: contents, so the padding
|
|
169
|
+
// wrapper below it is STILL the viewport's only layout box.
|
|
170
|
+
const scope = viewport!.firstElementChild as HTMLElement;
|
|
171
|
+
expect(scope.classList.contains("hk-scroll-progress-dock-scope")).toBe(true);
|
|
172
|
+
const inner = scope.firstElementChild as HTMLElement;
|
|
162
173
|
expect(inner.style.padding).toBe("2rem");
|
|
163
174
|
expect(inner.textContent).toContain("CONTENT");
|
|
164
175
|
});
|
|
165
176
|
});
|
|
166
177
|
|
|
178
|
+
describe("HkFaceShell scroll-progress dock master switch", () => {
|
|
179
|
+
const makeProbe = (box: { seen: unknown }) =>
|
|
180
|
+
defineComponent({
|
|
181
|
+
setup() {
|
|
182
|
+
box.seen = inject(HK_SCROLL_PROGRESS_ALLOWED, null);
|
|
183
|
+
return () => h("main", null, "CONTENT");
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("provides ALLOWED=true by default and mounts the fallback dock", () => {
|
|
188
|
+
const box: { seen: unknown } = { seen: "unset" };
|
|
189
|
+
const c = mount(h(HkFaceShell, {
|
|
190
|
+
navTitle: "Navigation",
|
|
191
|
+
}, {
|
|
192
|
+
header: () => null,
|
|
193
|
+
content: () => h(makeProbe(box)),
|
|
194
|
+
}));
|
|
195
|
+
// The `scrollProgress` prop (default true) is the face-level lever
|
|
196
|
+
// over every dock in the subtree.
|
|
197
|
+
expect(box.seen).toBe(true);
|
|
198
|
+
expect(c.querySelector(".hk-scroll-progress-dock-scope")).not.toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("scrollProgress:false closes the switch and stands the fallback dock down", () => {
|
|
202
|
+
const box: { seen: unknown } = { seen: "unset" };
|
|
203
|
+
const c = mount(h(HkFaceShell, {
|
|
204
|
+
navTitle: "Navigation",
|
|
205
|
+
scrollProgress: false,
|
|
206
|
+
}, {
|
|
207
|
+
header: () => null,
|
|
208
|
+
content: () => h(makeProbe(box)),
|
|
209
|
+
}));
|
|
210
|
+
expect(box.seen).toBe(false);
|
|
211
|
+
// Fully lazy: no scope, no pill anywhere in the shell.
|
|
212
|
+
expect(c.querySelector(".hk-scroll-progress-dock-scope")).toBeNull();
|
|
213
|
+
expect(c.querySelector(".hk-scroll-progress-dock")).toBeNull();
|
|
214
|
+
expect(c.querySelector(".hk-scroll-container-viewport")!.textContent).toContain("CONTENT");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
167
218
|
describe("the extraction boundary: the action-bar channel", () => {
|
|
168
219
|
it("HkFaceShell's header slot carries no actions param (no bar provided)", () => {
|
|
169
220
|
const seen: Array<Record<string, unknown>> = [];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { computed, defineComponent, ref, watch, type PropType } from "vue";
|
|
1
|
+
import { computed, defineComponent, provide, ref, watch, type PropType } from "vue";
|
|
2
2
|
import { HkCrossfade, HDrawer, HScrollContainer, useBreakpoint } from "@celestia-island/hikari";
|
|
3
3
|
import { useI18n } from "../i18n/context";
|
|
4
4
|
import { useSidebarRail } from "../composables/useSidebarRail";
|
|
5
5
|
import { provideMenuGroupState } from "../composables/useMenuGroupState";
|
|
6
6
|
import HkSidebarRailSwap from "./HkSidebarRailSwap";
|
|
7
7
|
import HkSidebarRailToggle, { isPointerOverRailToggle } from "./HkSidebarRailToggle";
|
|
8
|
+
import HkScrollProgressDock, { HK_SCROLL_PROGRESS_ALLOWED } from "./HkScrollProgressDock";
|
|
8
9
|
|
|
9
10
|
/** CSS-wide keywords. A shorthand rebuilt from one of them is NOT a
|
|
10
11
|
* padding (`inherit 0 inherit` is rejected by every engine, measured in
|
|
@@ -236,13 +237,19 @@ export const HkFaceShell = defineComponent({
|
|
|
236
237
|
* `stage` renders the content bare — the face owns scrolling,
|
|
237
238
|
* canvases and floating layers. */
|
|
238
239
|
contentMode: { type: String as PropType<"document" | "stage">, default: "document" },
|
|
239
|
-
/** Scroll-progress
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
* `
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
240
|
+
/** Scroll-progress DOCK master switch (2026-11-09): the shell-level
|
|
241
|
+
* on/off lever for every HkScrollProgressDock in the face —
|
|
242
|
+
* provided to the subtree as `HK_SCROLL_PROGRESS_ALLOWED`, so a
|
|
243
|
+
* `false` here turns a page's own dock lazy too (the shell >
|
|
244
|
+
* page-level `progressDock` > the dock's own `disabled`). While on
|
|
245
|
+
* (the default), the shell ALSO mounts its own fallback dock
|
|
246
|
+
* around the document content: unmigrated pages and bare lists
|
|
247
|
+
* still float the pill with zero per-page work; a standard page's
|
|
248
|
+
* own dock nests INSIDE the fallback and is the side that stands
|
|
249
|
+
* down (the nested-dock rule serves the outermost enabled dock —
|
|
250
|
+
* one pill, never two). Lists register themselves (HkTable /
|
|
251
|
+
* HkCardList / HkAdminTablePage do it built-in). Pass `false` to
|
|
252
|
+
* run the face bare. */
|
|
246
253
|
scrollProgress: { type: Boolean, default: true },
|
|
247
254
|
/** Content-area padding (inside the scroll viewport). Document mode
|
|
248
255
|
* only — a stage face owns its own geometry. Padding is applied to
|
|
@@ -302,6 +309,12 @@ export const HkFaceShell = defineComponent({
|
|
|
302
309
|
// same one as the desktop sidebar.
|
|
303
310
|
provideMenuGroupState();
|
|
304
311
|
|
|
312
|
+
// ── scroll-progress dock master switch ───────────────────────────
|
|
313
|
+
// The `scrollProgress` prop is now the FACE-LEVEL lever over every
|
|
314
|
+
// dock in the subtree (see the prop doc): provided once, statically
|
|
315
|
+
// — hosts pass it statically, the HkScrollContainer prop precedent.
|
|
316
|
+
provide(HK_SCROLL_PROGRESS_ALLOWED, props.scrollProgress);
|
|
317
|
+
|
|
305
318
|
// ── desktop icon rail + hover peek (sidebarCollapsible) ──────────
|
|
306
319
|
// SEMI-CONTROLLED rail state: a local session-only ref is the live
|
|
307
320
|
// state, so the toggle works with NO channel bound at all (the
|
|
@@ -545,11 +558,22 @@ export const HkFaceShell = defineComponent({
|
|
|
545
558
|
{props.contentMode === "stage" ? (
|
|
546
559
|
renderContentStage()
|
|
547
560
|
) : (
|
|
548
|
-
<HScrollContainer class="s-admin-shell-scroll"
|
|
549
|
-
{/*
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
561
|
+
<HScrollContainer class="s-admin-shell-scroll">
|
|
562
|
+
{/* The shell's FALLBACK dock (2026-11-09): wraps the
|
|
563
|
+
padded content so unmigrated pages and bare lists
|
|
564
|
+
still float the pill. A page that mounts its own dock
|
|
565
|
+
(HkAdminTablePage's, by default) nests INSIDE this
|
|
566
|
+
one and is the side that stands down — the nested
|
|
567
|
+
-dock rule serves the OUTERMOST enabled dock, so the
|
|
568
|
+
fallback keeps sensing and one pill paints, never
|
|
569
|
+
two. The scope is display: contents — the padding
|
|
570
|
+
wrapper below stays the viewport's only layout box. */}
|
|
571
|
+
<HkScrollProgressDock>
|
|
572
|
+
{/* Padding lives INSIDE the scroll viewport (an inner
|
|
573
|
+
wrapper) so card box-shadows are not clipped at the
|
|
574
|
+
viewport edges. */}
|
|
575
|
+
<div style={contentStyle.value}>{renderContentStage()}</div>
|
|
576
|
+
</HkScrollProgressDock>
|
|
553
577
|
</HScrollContainer>
|
|
554
578
|
)}
|
|
555
579
|
</main>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createApp, defineComponent, h, nextTick, reactive, ref } from "vue";
|
|
2
|
+
import { createApp, defineComponent, h, inject, nextTick, reactive, ref } from "vue";
|
|
3
3
|
|
|
4
4
|
import HkScrollContainer from "./HkScrollContainer";
|
|
5
|
+
import { HK_SCROLL_PROGRESS_HOST, HK_SCROLL_PROGRESS_REGISTRAR, HK_SCROLL_PROGRESS_VIEWPORT } from "./HkScrollProgressDock";
|
|
5
6
|
import type { OverlayScrollbarMarker } from "../composables/useOverlayScrollbar";
|
|
6
7
|
|
|
7
8
|
interface Mounted {
|
|
@@ -648,3 +649,47 @@ describe("HkScrollContainer change-mark rail", () => {
|
|
|
648
649
|
expect(track?.querySelectorAll(".hk-scrollbar-marker").length ?? 0).toBe(0);
|
|
649
650
|
});
|
|
650
651
|
});
|
|
652
|
+
|
|
653
|
+
describe("HkScrollContainer dock publication (2026-11-09)", () => {
|
|
654
|
+
it("publishes viewport + host refs and NO scroll-progress registrar", () => {
|
|
655
|
+
// The pill feature moved into the standalone HkScrollProgressDock:
|
|
656
|
+
// the container's only remaining role is publishing the two refs
|
|
657
|
+
// any dock in its slot injects (the one-way key dependency). Pin
|
|
658
|
+
// BOTH directions with probe injects: the dock keys resolve, the
|
|
659
|
+
// retired registrar key does not — a container that regrows the old
|
|
660
|
+
// provide (or drops the new ones) turns this red.
|
|
661
|
+
const seen = {
|
|
662
|
+
viewport: "unset" as unknown,
|
|
663
|
+
host: "unset" as unknown,
|
|
664
|
+
registrar: "unset" as unknown,
|
|
665
|
+
};
|
|
666
|
+
const Probe = defineComponent({
|
|
667
|
+
setup() {
|
|
668
|
+
seen.viewport = inject(HK_SCROLL_PROGRESS_VIEWPORT, null);
|
|
669
|
+
seen.host = inject(HK_SCROLL_PROGRESS_HOST, null);
|
|
670
|
+
seen.registrar = inject(HK_SCROLL_PROGRESS_REGISTRAR, null);
|
|
671
|
+
return () => h("div", "probe");
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
const host = document.createElement("div");
|
|
675
|
+
document.body.appendChild(host);
|
|
676
|
+
containers.push(host);
|
|
677
|
+
const app = createApp({ render: () => h(HkScrollContainer, {}, { default: () => h(Probe) }) });
|
|
678
|
+
mounts.push(app);
|
|
679
|
+
app.mount(host);
|
|
680
|
+
const root = host.querySelector<HTMLElement>(".hk-scroll-container");
|
|
681
|
+
const viewport = host.querySelector<HTMLElement>(".hk-scroll-container-viewport");
|
|
682
|
+
expect(root).not.toBeNull();
|
|
683
|
+
expect(viewport).not.toBeNull();
|
|
684
|
+
expect(seen.registrar).toBeNull();
|
|
685
|
+
// The refs are the shared `Ref<HTMLElement | null>` contract, and
|
|
686
|
+
// by probe time the template refs have landed: they point at THIS
|
|
687
|
+
// container's viewport / host elements.
|
|
688
|
+
const viewportRef = seen.viewport as { value: unknown } | null;
|
|
689
|
+
const hostRef = seen.host as { value: unknown } | null;
|
|
690
|
+
expect(viewportRef).not.toBeNull();
|
|
691
|
+
expect(hostRef).not.toBeNull();
|
|
692
|
+
expect(viewportRef!.value).toBe(viewport);
|
|
693
|
+
expect(hostRef!.value).toBe(root);
|
|
694
|
+
});
|
|
695
|
+
});
|