@celestia-island/hikari 0.55.149 → 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 +1 -1
- package/src/animation/registerAnimations.ts +0 -1
- package/src/components/HkConnectionStatus.shell.test.tsx +34 -0
- package/src/components/HkConnectionStatus.tsx +31 -10
- package/src/components/HkKanban.tsx +10 -0
- package/src/components/HkScrollContainer.dock.test.tsx +89 -0
- package/src/components/HkScrollContainer.scss +50 -0
- package/src/components/HkScrollContainer.tsx +47 -0
- package/src/components/HkThemeDecor.test.tsx +11 -22
- package/src/components/HkThemeDecor.tsx +1 -1
- package/src/components/StatusTrayDecor.mount.test.ts +35 -0
- package/src/components/{HkStatusTray.size.test.ts → StatusTrayDecor.size.test.ts} +7 -3
- package/src/components/{HkStatusTray.test.tsx → StatusTrayDecor.test.tsx} +3 -2
- package/src/components/themeDecorLayout.contract.test.ts +6 -6
- package/src/composables/engineHealthTypes.ts +50 -0
- package/src/i18n/usageSurface.test.ts +0 -1
- package/src/index.ts +2 -102
- package/src/theme/standardDecor.test.ts +14 -13
- package/src/theme/standardDecor.ts +7 -3
- package/src/theme/themeDecorSurface.test.ts +5 -3
- package/src/utils/format.ts +0 -11
- package/src/components/HkAttachmentModal.scss +0 -49
- package/src/components/HkAttachmentModal.tsx +0 -318
- package/src/components/HkChatTypes.ts +0 -65
- package/src/components/HkContextRing.scss +0 -136
- package/src/components/HkContextRing.test.tsx +0 -309
- package/src/components/HkContextRing.tsx +0 -276
- package/src/components/HkModelCatalog.test.ts +0 -81
- package/src/components/HkModelCatalog.ts +0 -330
- package/src/components/HkModelTag.scss +0 -171
- package/src/components/HkModelTag.test.tsx +0 -155
- package/src/components/HkModelTag.tsx +0 -219
- package/src/components/HkRichInput.scss +0 -178
- package/src/components/HkRichInput.tsx +0 -335
- package/src/components/HkStatusTray.scss +0 -36
- package/src/components/HkStatusTray.tsx +0 -187
- package/src/components/HkTokenUsageBadge.scss +0 -50
- package/src/components/HkTokenUsageBadge.test.tsx +0 -59
- package/src/components/HkTokenUsageBadge.tsx +0 -54
- package/src/components/HkTokenUsagePanel.scss +0 -100
- package/src/components/HkTokenUsagePanel.tsx +0 -140
- package/src/components/HkToolBlock.scss +0 -327
- package/src/components/HkToolBlock.tsx +0 -394
- package/src/components/HkToolBlock.windowScroll.contract.test.ts +0 -93
- package/src/components/HkVoiceInputPopup.scss +0 -82
- package/src/components/HkVoiceInputPopup.tsx +0 -97
- package/src/composables/useAttachments.ts +0 -154
- package/src/composables/useConnectionProbe.test.ts +0 -179
- package/src/composables/useConnectionProbe.ts +0 -201
- package/src/composables/useEngineHealth.test.ts +0 -128
- package/src/composables/useEngineHealth.ts +0 -93
- package/src/composables/useSendShortcut.ts +0 -35
- package/src/i18n/contextKeys.test.ts +0 -52
- package/src/i18n/locales/ar/chat.json +0 -49
- package/src/i18n/locales/de/chat.json +0 -49
- package/src/i18n/locales/en/chat.json +0 -49
- package/src/i18n/locales/es/chat.json +0 -49
- package/src/i18n/locales/fr/chat.json +0 -49
- package/src/i18n/locales/ja/chat.json +0 -49
- package/src/i18n/locales/ko/chat.json +0 -49
- package/src/i18n/locales/pt/chat.json +0 -49
- package/src/i18n/locales/ru/chat.json +0 -49
- package/src/i18n/locales/zh-Hans/chat.json +0 -49
- package/src/i18n/locales/zh-Hant/chat.json +0 -49
package/package.json
CHANGED
|
@@ -110,7 +110,6 @@ registerCssAnimation("hk-flip-out");
|
|
|
110
110
|
registerCssAnimation("hk-flip-in");
|
|
111
111
|
// components/HkVoiceInputPopup.scss — voice waveform bounce (loops
|
|
112
112
|
// forever while listening; upstreamed from chest's plana-legacy).
|
|
113
|
-
registerCssAnimation("s-voice-wave-bounce", { infinite: true });
|
|
114
113
|
// components/HkPlaceholderMarquee.scss — overflowing placeholder sweep
|
|
115
114
|
// (loops forever; pure CSS, loop geometry on inline custom properties).
|
|
116
115
|
registerCssAnimation("hk-placeholder-marquee-scroll", { infinite: true });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import { HkConnectionStatus } from "./HkConnectionStatus";
|
|
5
|
+
|
|
6
|
+
/** The shell contract: WITHOUT injections the component never touches
|
|
7
|
+
* the network — the fetch-poll was demoted with the chat kit (the M1
|
|
8
|
+
* mutation guard: a fetch in the fallback path must go red here). */
|
|
9
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
12
|
+
app.unmount();
|
|
13
|
+
container.remove();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("HkConnectionStatus (shell)", () => {
|
|
18
|
+
it("renders an inert unknown state with zero network calls", () => {
|
|
19
|
+
const fetchSpy = vi.fn();
|
|
20
|
+
const holder = globalThis.fetch;
|
|
21
|
+
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
22
|
+
try {
|
|
23
|
+
const container = document.createElement("div");
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
const app = createApp({ render: () => h(HkConnectionStatus, { standalone: true }) });
|
|
26
|
+
app.mount(container);
|
|
27
|
+
mounts.push({ app, container });
|
|
28
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
29
|
+
expect(container.textContent ?? "").not.toBeNull();
|
|
30
|
+
} finally {
|
|
31
|
+
globalThis.fetch = holder;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { computed, defineComponent, inject, type InjectionKey, type PropType, type Ref } from "vue";
|
|
1
|
+
import { computed, defineComponent, inject, ref, type InjectionKey, type PropType, type Ref } from "vue";
|
|
2
2
|
|
|
3
3
|
import { useI18n } from "../i18n/context";
|
|
4
|
-
import {
|
|
5
|
-
import { useEngineHealth } from "../composables/useEngineHealth";
|
|
4
|
+
import type { EngineHealth, EngineNetworkInfo, ProbeResult } from "../composables/engineHealthTypes";
|
|
6
5
|
|
|
7
6
|
import { HkStatusBar } from "./HkStatusBar";
|
|
8
7
|
import {
|
|
@@ -28,6 +27,25 @@ export interface HkConnectionProbeSource {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/** Injection key for the connection-probe overload (see above). */
|
|
30
|
+
/** Engine-health source a host provides (the demoted fetch's
|
|
31
|
+
* replacement): a ref of the /api/health envelope, or null. */
|
|
32
|
+
export const HK_ENGINE_HEALTH: InjectionKey<Ref<EngineHealth | null>> = Symbol(
|
|
33
|
+
"HK_ENGINE_HEALTH",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/** The shell's inert probe — no network, no timers: "unknown". */
|
|
37
|
+
const INERT_PROBE: ProbeResult = {
|
|
38
|
+
connected: false,
|
|
39
|
+
state: "disconnected",
|
|
40
|
+
transportTier: "unknown",
|
|
41
|
+
tier: "unknown",
|
|
42
|
+
latencyMs: null,
|
|
43
|
+
attemptNumber: 0,
|
|
44
|
+
retryCount: 0,
|
|
45
|
+
retryTotal: 0,
|
|
46
|
+
countdown: 0,
|
|
47
|
+
};
|
|
48
|
+
|
|
31
49
|
export const HK_CONNECTION_PROBE: InjectionKey<HkConnectionProbeSource> =
|
|
32
50
|
Symbol("HkConnectionProbe");
|
|
33
51
|
|
|
@@ -45,7 +63,8 @@ const backendStateColor: Record<HkBackendStatus["state"], string> = {
|
|
|
45
63
|
* Composes useConnectionProbe + useEngineHealth + useConnectionInfo +
|
|
46
64
|
* HkStatusBar. The probe source is injectable: when a parent provides
|
|
47
65
|
* HK_CONNECTION_PROBE, that probe (and its retryNow) is used verbatim;
|
|
48
|
-
*
|
|
66
|
+
* without an injection the shell renders an inert unknown state and
|
|
67
|
+
* NEVER touches the network (pinned). Slots:
|
|
49
68
|
* - `left` — extras after the status tags (e.g. a mock badge)
|
|
50
69
|
* - `right` — extras on the far right (e.g. an engine version tag)
|
|
51
70
|
*/
|
|
@@ -54,19 +73,21 @@ export const HkConnectionStatus = defineComponent({
|
|
|
54
73
|
props: {
|
|
55
74
|
version: { type: String, default: "0.1.0" },
|
|
56
75
|
panelBuildHash: { type: String as PropType<string | undefined>, default: undefined },
|
|
57
|
-
/** API origin for the /api/health probe (defaults to same-origin ""). */
|
|
58
|
-
baseUrl: { type: String as PropType<string | undefined>, default: undefined },
|
|
59
76
|
backendStatus: { type: Object as PropType<HkBackendStatus | undefined>, default: undefined },
|
|
60
77
|
standalone: { type: Boolean, default: true },
|
|
61
78
|
/** Forwarded to HkStatusBar — collapse to the traffic-light dot. */
|
|
62
79
|
compact: { type: Boolean, default: false },
|
|
63
80
|
},
|
|
64
81
|
setup(props, { slots }) {
|
|
65
|
-
//
|
|
66
|
-
//
|
|
82
|
+
// SHELL-ONLY: the network side (WHO polls /api/health and HOW)
|
|
83
|
+
// lives in the host — injected via HK_CONNECTION_PROBE /
|
|
84
|
+
// HK_ENGINE_HEALTH. Without an injection the shell renders an
|
|
85
|
+
// inert unknown state and never touches the network (the
|
|
86
|
+
// fetch-poll defaults were demoted with the chat kit wave).
|
|
67
87
|
const injectedProbe = inject(HK_CONNECTION_PROBE, null);
|
|
68
|
-
const
|
|
69
|
-
const
|
|
88
|
+
const probe = injectedProbe?.result ?? ref(INERT_PROBE);
|
|
89
|
+
const retryNow = injectedProbe?.retryNow ?? (() => {});
|
|
90
|
+
const health = inject(HK_ENGINE_HEALTH, ref<EngineHealth | null>(null));
|
|
70
91
|
|
|
71
92
|
const connectionState = computed<ConnectionStateInput>(() => probe.value.state);
|
|
72
93
|
const transportTier = computed(() => probe.value.transportTier);
|
|
@@ -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"
|
|
@@ -175,29 +175,18 @@ describe("HkThemeDecor", () => {
|
|
|
175
175
|
expect(seen.at(-1)).toMatchObject({ variant: "host", kept: 1 });
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
it("
|
|
179
|
-
// The
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const themedTray = defineComponent({
|
|
186
|
-
name: "ThemedTray",
|
|
187
|
-
setup: () => () => h("i", { "data-decor": "themed-tray" }),
|
|
178
|
+
it("renders a theme tray over the now-empty builtin floor", async () => {
|
|
179
|
+
// The builtin status.tray floor is EMPTY since the demotion wave —
|
|
180
|
+
// the default theme package fills it via the "*" wildcard, and any
|
|
181
|
+
// exact-theme registration wins over that. Here: only the themed
|
|
182
|
+
// tray renders.
|
|
183
|
+
const ThemedTray = defineComponent({
|
|
184
|
+
setup: () => () => h("div", { "data-decor": "themed-tray" }),
|
|
188
185
|
});
|
|
189
|
-
registerThemeDecor({ themeId: "
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
setTheme("endfield");
|
|
194
|
-
await nextTick();
|
|
195
|
-
expect(container.querySelector('[data-decor="themed-tray"]')).not.toBeNull();
|
|
196
|
-
expect(container.querySelector(".s-status-bar-system-tray")).toBeNull();
|
|
197
|
-
|
|
198
|
-
setTheme("default");
|
|
186
|
+
registerThemeDecor({ themeId: "t", slot: "status.tray", component: ThemedTray });
|
|
187
|
+
useTheme().setTheme("t");
|
|
188
|
+
const host = mount(h(HkThemeDecor, { slot: "status.tray" }));
|
|
199
189
|
await nextTick();
|
|
200
|
-
expect(
|
|
201
|
-
expect(container.querySelector(".s-status-bar-system-tray")).not.toBeNull();
|
|
190
|
+
expect(host.querySelector('[data-decor="themed-tray"]')).not.toBeNull();
|
|
202
191
|
});
|
|
203
192
|
});
|
|
@@ -50,7 +50,7 @@ import { useTheme } from "../theme/useTheme";
|
|
|
50
50
|
* that wants numeric sizing declares its own prop and receives it through
|
|
51
51
|
* `decorProps`.
|
|
52
52
|
*
|
|
53
|
-
* The ladder is the CONTRACT's, not any one decor's:
|
|
53
|
+
* The ladder is the CONTRACT's, not any one decor's: the default theme's tray
|
|
54
54
|
* implements `sm | md | lg` only, so asking it for `xs` publishes no
|
|
55
55
|
* `--hk-tray-*` var at all and the sheet's `var(--hk-tray-glyph, 9px)`
|
|
56
56
|
* fallbacks render the shipped (md) geometry. That degradation is pinned by
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import StatusTrayDecor from "../../../default_theme/src/StatusTrayDecor";
|
|
4
|
+
// The one-import contract under test: importing the package index IS the
|
|
5
|
+
// mount (README: `import "@celestia-island/default-theme"` — CSS + decor).
|
|
6
|
+
import "../../../default_theme/src/index";
|
|
7
|
+
import { getThemeDecor } from "../theme/themeDecor";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @celestia-island/default-theme's delivery mechanism: a bare import of the
|
|
11
|
+
* package mounts the tray decor on the "*" wildcard via registerThemeDecor,
|
|
12
|
+
* so every theme (and themeless hosts) resolve it without any host glue.
|
|
13
|
+
*
|
|
14
|
+
* This pin lives in the vue suite because default_theme ships no runner of
|
|
15
|
+
* its own — the vue suite is the only vitest root (CI: ci-hosted.yml) — and
|
|
16
|
+
* the registry it asserts against is hikari's own. It goes red if the
|
|
17
|
+
* side-effect call disappears from the package index (the "explicit
|
|
18
|
+
* mountDefaultTheme()" escape hatch is a replacement, not a supplement).
|
|
19
|
+
*/
|
|
20
|
+
describe("@celestia-island/default-theme side-effect mount", () => {
|
|
21
|
+
it("a bare import of the package mounts the tray on the '*' wildcard", () => {
|
|
22
|
+
const resolved = getThemeDecor("status.tray", "any-theme-id");
|
|
23
|
+
expect(resolved, "status.tray resolves after the bare import").toBeDefined();
|
|
24
|
+
// And it is THE decor component — not merely some entry — so the
|
|
25
|
+
// wildcard registration cannot silently drift to another component.
|
|
26
|
+
expect(resolved?.component).toBe(StatusTrayDecor);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("outranks the builtin floor without touching it (the override path)", () => {
|
|
30
|
+
// The builtin floor for status.tray is deliberately empty (pinned in
|
|
31
|
+
// standardDecor.test.ts); the wildcard entry is the ONLY reason the
|
|
32
|
+
// slot resolves. Same lookup through the star id lands on it too.
|
|
33
|
+
expect(getThemeDecor("status.tray", "*")?.component).toBe(StatusTrayDecor);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -6,7 +6,7 @@ import { compile } from "sass";
|
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
7
|
import { createApp, h } from "vue";
|
|
8
8
|
|
|
9
|
-
import HkStatusTray, { HK_TRAY_SIZE_VARS, type HkStatusTraySize } from "
|
|
9
|
+
import HkStatusTray, { HK_TRAY_SIZE_VARS, type HkStatusTraySize } from "../../../default_theme/src/StatusTrayDecor";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The "md is pixel-identical to what already ships" contract.
|
|
@@ -18,7 +18,7 @@ import HkStatusTray, { HK_TRAY_SIZE_VARS, type HkStatusTraySize } from "./HkStat
|
|
|
18
18
|
*
|
|
19
19
|
* 1. the shipped admin-tokens rules (the source of truth),
|
|
20
20
|
* 2. the component's `md` row in HK_TRAY_SIZE_VARS,
|
|
21
|
-
* 3. the fallbacks in
|
|
21
|
+
* 3. the fallbacks in StatusTrayDecor.scss (packages/default_theme).
|
|
22
22
|
*
|
|
23
23
|
* This guard reads (1) out of the COMPILED sheet — not a hand-copied list —
|
|
24
24
|
* and holds (2) and (3) to it, so moving any one of them alone fails here
|
|
@@ -67,7 +67,11 @@ function specificity(selector: string): number {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const adminCss = compileSheet(resolve(stylesDir, "admin-tokens.scss"));
|
|
70
|
-
|
|
70
|
+
// The tray decor itself lives in packages/default_theme now; the shared
|
|
71
|
+
// sheets it is held against (admin-tokens.scss, scale.scss) stayed here.
|
|
72
|
+
const trayCss = compileSheet(
|
|
73
|
+
resolve(componentDir, "../../../default_theme/src/StatusTrayDecor.scss"),
|
|
74
|
+
);
|
|
71
75
|
const scaleVars: Record<string, string> = {};
|
|
72
76
|
for (const m of readFileSync(resolve(srcDir, "scale.scss"), "utf8")
|
|
73
77
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { createApp, h, nextTick } from "vue";
|
|
3
3
|
|
|
4
4
|
import { readHkRuntime } from "../runtime/registry";
|
|
5
|
-
import HkStatusTray from "
|
|
5
|
+
import HkStatusTray from "../../../default_theme/src/StatusTrayDecor";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* HkStatusTray behaviour: the gamepad cluster + clock surface, its size and
|
|
@@ -10,7 +10,8 @@ import HkStatusTray from "./HkStatusTray";
|
|
|
10
10
|
* teardown of both intervals.
|
|
11
11
|
*
|
|
12
12
|
* The "md equals the shipped CSS" contract lives next door in
|
|
13
|
-
* `
|
|
13
|
+
* `StatusTrayDecor.size.test.ts` (it reads the sheets, not the DOM —
|
|
14
|
+
* the decor itself lives in packages/default_theme).
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
@@ -50,7 +50,6 @@ interface Target {
|
|
|
50
50
|
|
|
51
51
|
const TARGETS: Target[] = [
|
|
52
52
|
{ file: "HkThemeDecor.tsx", anchor: "HkThemeDecor" },
|
|
53
|
-
{ file: "HkStatusTray.tsx", anchor: "s-status-bar-system-tray" },
|
|
54
53
|
{ file: "HkStatusTray.scss", anchor: "s-status-bar-system-tray" },
|
|
55
54
|
];
|
|
56
55
|
|
|
@@ -102,10 +101,11 @@ describe("theme decor layout contract", () => {
|
|
|
102
101
|
});
|
|
103
102
|
|
|
104
103
|
it("scans existing files that carry the expected content (no empty scan)", () => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
// The tray decor moved to packages/default_theme (the demotion
|
|
105
|
+
// wave); only the files still living here are scanned.
|
|
106
|
+
const present = TARGETS.filter(({ file }) => existsSync(resolve(componentDir, file)));
|
|
107
|
+
expect(present.length, "at least one target still in-tree").toBeGreaterThan(0);
|
|
108
|
+
for (const { file, anchor } of present) {
|
|
109
109
|
const source = read(file);
|
|
110
110
|
expect(source.length, `${file} is not empty`).toBeGreaterThan(50);
|
|
111
111
|
expect(source, `${file} carries ${anchor}`).toContain(anchor);
|
|
@@ -113,7 +113,7 @@ describe("theme decor layout contract", () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it("keeps positioning out of the decor components and their sheets", () => {
|
|
116
|
-
for (const { file } of TARGETS) {
|
|
116
|
+
for (const { file } of TARGETS.filter(({ file }) => existsSync(resolve(componentDir, file)))) {
|
|
117
117
|
const hits = positionHits(file, read(file));
|
|
118
118
|
expect(hits, `${file} must not position itself: ${JSON.stringify(hits)}`).toEqual([]);
|
|
119
119
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Engine-health DTOs — the SHELL side of the split (the fetch hook
|
|
2
|
+
* that fills them was demoted to the consuming app with the chat-kit
|
|
3
|
+
* wave; the shapes stay so HkConnectionStatus's injection contract is
|
|
4
|
+
* typed here). */
|
|
5
|
+
|
|
6
|
+
/** Connectivity probe result — the shape a host's injected probe
|
|
7
|
+
* (HK_CONNECTION_PROBE) exposes. The fetch implementation that
|
|
8
|
+
* produced it was demoted to the app; the shape stays as the shell's
|
|
9
|
+
* contract. */
|
|
10
|
+
export interface ProbeResult {
|
|
11
|
+
connected: boolean;
|
|
12
|
+
state: "connected" | "disconnected" | "connecting" | "reconnecting" | "failed";
|
|
13
|
+
/** Canonical transport tier ("local" | "ws" | "sse" | "poll"). */
|
|
14
|
+
transportTier: string;
|
|
15
|
+
/** @deprecated Use `transportTier`. Alias kept for compatibility. */
|
|
16
|
+
tier: string;
|
|
17
|
+
latencyMs: number | null;
|
|
18
|
+
/** Canonical 1-based connect-attempt counter. */
|
|
19
|
+
attemptNumber: number;
|
|
20
|
+
/** @deprecated Use `attemptNumber`. Alias kept for compatibility. */
|
|
21
|
+
retryCount: number;
|
|
22
|
+
retryTotal: number;
|
|
23
|
+
/** Seconds until the next automatic probe attempt. */
|
|
24
|
+
countdown: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/** Inlined subset of the `/api/health` network payload — kept local so
|
|
29
|
+
* hikari stays dependency-free of protocol packages. */
|
|
30
|
+
export interface NetworkInfo {
|
|
31
|
+
transport?: string;
|
|
32
|
+
region?: string;
|
|
33
|
+
asn?: number | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Inlined subset of the `/api/health` response envelope. */
|
|
37
|
+
export interface HealthResponse {
|
|
38
|
+
engine_version?: string;
|
|
39
|
+
build_hash?: string;
|
|
40
|
+
network?: NetworkInfo;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type { NetworkInfo as EngineNetworkInfo };
|
|
44
|
+
|
|
45
|
+
/** Subset of `/api/health` relevant to status-bar style consumers. */
|
|
46
|
+
export interface EngineHealth {
|
|
47
|
+
engineVersion: string | null;
|
|
48
|
+
engineBuildHash: string | null;
|
|
49
|
+
network: NetworkInfo | null;
|
|
50
|
+
}
|
|
@@ -122,7 +122,6 @@ function scanSources(): { statics: Site[]; constructed: Site[] } {
|
|
|
122
122
|
* every listed family must still exist in source, so this whitelist
|
|
123
123
|
* cannot rot into silence. */
|
|
124
124
|
const DYNAMIC_KEY_FAMILIES: Record<string, string> = {
|
|
125
|
-
"hikari::context.": "HkContextRing section keys — closed enum, pinned by name in contextKeys.test.ts.",
|
|
126
125
|
"hikari::statusBar.backend.": "HkConnectionStatus — enumerated connection-state suffixes.",
|
|
127
126
|
"hikari::statusBar.region.": "HkStatusBar region names — OPTIONAL per-locale overrides over Intl.DisplayNames (see LOCALE_SPECIFIC_NESTED in flatKeyParity.test.ts).",
|
|
128
127
|
"hikari::statusBar.tier.": "HkStatusBar transport tiers — constructed into a variable at a distance from its t() call (F1); the four concrete ids are pinned by name in CONSTRUCTED_FAMILY_KEYS below.",
|