@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.
Files changed (64) hide show
  1. package/package.json +1 -1
  2. package/src/animation/registerAnimations.ts +0 -1
  3. package/src/components/HkConnectionStatus.shell.test.tsx +34 -0
  4. package/src/components/HkConnectionStatus.tsx +31 -10
  5. package/src/components/HkKanban.tsx +10 -0
  6. package/src/components/HkScrollContainer.dock.test.tsx +89 -0
  7. package/src/components/HkScrollContainer.scss +50 -0
  8. package/src/components/HkScrollContainer.tsx +47 -0
  9. package/src/components/HkThemeDecor.test.tsx +11 -22
  10. package/src/components/HkThemeDecor.tsx +1 -1
  11. package/src/components/StatusTrayDecor.mount.test.ts +35 -0
  12. package/src/components/{HkStatusTray.size.test.ts → StatusTrayDecor.size.test.ts} +7 -3
  13. package/src/components/{HkStatusTray.test.tsx → StatusTrayDecor.test.tsx} +3 -2
  14. package/src/components/themeDecorLayout.contract.test.ts +6 -6
  15. package/src/composables/engineHealthTypes.ts +50 -0
  16. package/src/i18n/usageSurface.test.ts +0 -1
  17. package/src/index.ts +2 -102
  18. package/src/theme/standardDecor.test.ts +14 -13
  19. package/src/theme/standardDecor.ts +7 -3
  20. package/src/theme/themeDecorSurface.test.ts +5 -3
  21. package/src/utils/format.ts +0 -11
  22. package/src/components/HkAttachmentModal.scss +0 -49
  23. package/src/components/HkAttachmentModal.tsx +0 -318
  24. package/src/components/HkChatTypes.ts +0 -65
  25. package/src/components/HkContextRing.scss +0 -136
  26. package/src/components/HkContextRing.test.tsx +0 -309
  27. package/src/components/HkContextRing.tsx +0 -276
  28. package/src/components/HkModelCatalog.test.ts +0 -81
  29. package/src/components/HkModelCatalog.ts +0 -330
  30. package/src/components/HkModelTag.scss +0 -171
  31. package/src/components/HkModelTag.test.tsx +0 -155
  32. package/src/components/HkModelTag.tsx +0 -219
  33. package/src/components/HkRichInput.scss +0 -178
  34. package/src/components/HkRichInput.tsx +0 -335
  35. package/src/components/HkStatusTray.scss +0 -36
  36. package/src/components/HkStatusTray.tsx +0 -187
  37. package/src/components/HkTokenUsageBadge.scss +0 -50
  38. package/src/components/HkTokenUsageBadge.test.tsx +0 -59
  39. package/src/components/HkTokenUsageBadge.tsx +0 -54
  40. package/src/components/HkTokenUsagePanel.scss +0 -100
  41. package/src/components/HkTokenUsagePanel.tsx +0 -140
  42. package/src/components/HkToolBlock.scss +0 -327
  43. package/src/components/HkToolBlock.tsx +0 -394
  44. package/src/components/HkToolBlock.windowScroll.contract.test.ts +0 -93
  45. package/src/components/HkVoiceInputPopup.scss +0 -82
  46. package/src/components/HkVoiceInputPopup.tsx +0 -97
  47. package/src/composables/useAttachments.ts +0 -154
  48. package/src/composables/useConnectionProbe.test.ts +0 -179
  49. package/src/composables/useConnectionProbe.ts +0 -201
  50. package/src/composables/useEngineHealth.test.ts +0 -128
  51. package/src/composables/useEngineHealth.ts +0 -93
  52. package/src/composables/useSendShortcut.ts +0 -35
  53. package/src/i18n/contextKeys.test.ts +0 -52
  54. package/src/i18n/locales/ar/chat.json +0 -49
  55. package/src/i18n/locales/de/chat.json +0 -49
  56. package/src/i18n/locales/en/chat.json +0 -49
  57. package/src/i18n/locales/es/chat.json +0 -49
  58. package/src/i18n/locales/fr/chat.json +0 -49
  59. package/src/i18n/locales/ja/chat.json +0 -49
  60. package/src/i18n/locales/ko/chat.json +0 -49
  61. package/src/i18n/locales/pt/chat.json +0 -49
  62. package/src/i18n/locales/ru/chat.json +0 -49
  63. package/src/i18n/locales/zh-Hans/chat.json +0 -49
  64. package/src/i18n/locales/zh-Hant/chat.json +0 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.149",
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",
@@ -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 { useConnectionProbe, type ProbeResult } from "../composables/useConnectionProbe";
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
- * otherwise hikari's own fetch-poll useConnectionProbe() runs. Slots:
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
- // Probe-source overload: a host app (chest) provides its own
66
- // RpcClient-driven probe; everyone else gets hikari's fetch-poll.
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 { result: probe, retryNow } = injectedProbe ?? useConnectionProbe();
69
- const { health } = useEngineHealth(props.baseUrl);
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("lets a theme replace the built-in tray end to end (the TRAY_DECORS replacement path)", async () => {
179
- // The per-theme decoration table chest kept inside its tray component is
180
- // gone: a theme ships its own tray by registering a component for this
181
- // slot under its own id, and hikari's built-in tray answers for every
182
- // other theme.
183
- registerStandardThemeDecor();
184
- const { setTheme } = useTheme();
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: "endfield", slot: "status.tray", component: themedTray });
190
-
191
- const container = mount(h(HkThemeDecor, { slot: "status.tray" }));
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(container.querySelector('[data-decor="themed-tray"]')).toBeNull();
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: `HkStatusTray`
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 "./HkStatusTray";
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 HkStatusTray.scss.
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
- const trayCss = compileSheet(resolve(componentDir, "HkStatusTray.scss"));
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 "./HkStatusTray";
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
- * `HkStatusTray.size.test.ts` (it reads the sheets, not the DOM).
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
- expect(TARGETS.length).toBeGreaterThan(0);
106
- for (const { file, anchor } of TARGETS) {
107
- const path = resolve(componentDir, file);
108
- expect(existsSync(path), `${file} exists`).toBe(true);
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.",