@celestia-island/hikari 0.55.212 → 0.55.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.212",
3
+ "version": "0.55.213",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -37,6 +37,10 @@
37
37
  "./plugins": {
38
38
  "import": "./src/plugins/index.ts",
39
39
  "types": "./src/plugins/index.ts"
40
+ },
41
+ "./plugins/boot-chrome": {
42
+ "import": "./src/plugins/boot-chrome.mjs",
43
+ "types": "./src/plugins/boot-chrome.d.mts"
40
44
  }
41
45
  },
42
46
  "files": [
@@ -109,6 +109,74 @@ describe("HkTitleBar", () => {
109
109
  });
110
110
  });
111
111
 
112
+ describe("HkTitleBar window-chrome claim", () => {
113
+ // The bar IS the app's title bar, so mounting it must stand hikari's boot bar
114
+ // (boot-chrome.js) down — otherwise two caption bars overlap for the rest of
115
+ // the session. Tests use a raw window listener: that is the whole contract.
116
+ function collect(): { claims: Array<{ by?: string }>; stop: () => void } {
117
+ const claims: Array<{ by?: string }> = [];
118
+ const listener = (event: Event) => {
119
+ claims.push((event as CustomEvent<{ by?: string }>).detail ?? {});
120
+ };
121
+ window.addEventListener("hk:chrome-claim", listener);
122
+ return {
123
+ claims,
124
+ stop: () => window.removeEventListener("hk:chrome-claim", listener),
125
+ };
126
+ }
127
+
128
+ function cleanup(): void {
129
+ document.documentElement.removeAttribute("data-hk-chrome");
130
+ }
131
+
132
+ it("claims the window on mount and marks the app as the chrome owner", () => {
133
+ const { claims, stop } = collect();
134
+ try {
135
+ mount(h(HkTitleBar, { title: "App" }));
136
+
137
+ expect(claims).toHaveLength(1);
138
+ expect(claims[0]?.by).toBe("HkTitleBar");
139
+ expect(document.documentElement.getAttribute("data-hk-chrome")).toBe("app");
140
+ } finally {
141
+ stop();
142
+ cleanup();
143
+ }
144
+ });
145
+
146
+ it("never claims twice for one mount (a re-render is not a new takeover)", async () => {
147
+ const { claims, stop } = collect();
148
+ try {
149
+ const maximized = ref(false);
150
+ mount(h(() => h(HkTitleBar, { title: "App", maximized: maximized.value })));
151
+ expect(claims).toHaveLength(1);
152
+
153
+ maximized.value = true;
154
+ await nextTick();
155
+ maximized.value = false;
156
+ await nextTick();
157
+
158
+ expect(claims).toHaveLength(1);
159
+ } finally {
160
+ stop();
161
+ cleanup();
162
+ }
163
+ });
164
+
165
+ it("leaves the existing props/emits contract untouched", () => {
166
+ const emitted: string[] = [];
167
+ const host = mount(
168
+ h(HkTitleBar, {
169
+ title: "App",
170
+ onMinimize: () => emitted.push("minimize"),
171
+ onClose: () => emitted.push("close"),
172
+ }),
173
+ );
174
+ host.querySelector<HTMLButtonElement>('[title="Minimize"]')!.click();
175
+ host.querySelector<HTMLButtonElement>('[title="Close"]')!.click();
176
+ expect(emitted).toEqual(["minimize", "close"]);
177
+ });
178
+ });
179
+
112
180
  describe("HkTitleBar stylesheet contract", () => {
113
181
  const css = compile(resolve(componentDir, "HkTitleBar.scss"), {
114
182
  style: "expanded",
@@ -1,5 +1,6 @@
1
1
  import { defineComponent, onBeforeUnmount, onMounted, ref } from "vue";
2
2
  import { useI18n } from "../i18n/context";
3
+ import { claimWindowChrome } from "../runtime/windowChrome";
3
4
 
4
5
  import "./HkTitleBar.scss";
5
6
 
@@ -60,6 +61,16 @@ export default defineComponent({
60
61
  },
61
62
  setup(props, { emit, slots }) {
62
63
  const { t } = useI18n();
64
+ // The bar IS the app's window chrome, so mounting it means the boot chrome
65
+ // (boot-chrome.js, drawn before the bundle exists) must hand the window
66
+ // over: dispatch `hk:chrome-claim` once per mount, never revoked. See
67
+ // runtime/windowChrome for the contract; hydration/SSR never reaches here.
68
+ let claimed = false;
69
+ onMounted(() => {
70
+ if (claimed) return;
71
+ claimed = true;
72
+ claimWindowChrome("HkTitleBar");
73
+ });
63
74
  return () => (
64
75
  <div
65
76
  class="hk-titlebar"
@@ -1,5 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { createApp, defineComponent, h, type App } from "vue";
2
+ import { createApp, defineComponent, h, nextTick, type App } from "vue";
3
+
4
+ import { WINDOW_IPC } from "../runtime/windowChrome";
3
5
 
4
6
  import HkWindowControls, { isApplePlatform } from "./HkWindowControls";
5
7
 
@@ -91,3 +93,234 @@ describe("HkWindowControls (two deliberate modes, explicit None)", () => {
91
93
  t.app.unmount();
92
94
  });
93
95
  });
96
+
97
+ describe("HkWindowControls window-chrome claim", () => {
98
+ // Only a cluster that ACTUALLY renders may take the window over from
99
+ // hikari's boot bar: claiming while rendering nothing would remove the only
100
+ // usable controls in the window.
101
+ function collect(): { claims: Array<{ by?: string }>; stop: () => void } {
102
+ const claims: Array<{ by?: string }> = [];
103
+ const listener = (event: Event) => {
104
+ claims.push((event as CustomEvent<{ by?: string }>).detail ?? {});
105
+ };
106
+ window.addEventListener("hk:chrome-claim", listener);
107
+ return {
108
+ claims,
109
+ stop: () => window.removeEventListener("hk:chrome-claim", listener),
110
+ };
111
+ }
112
+
113
+ function cleanup(): void {
114
+ document.documentElement.removeAttribute("data-hk-chrome");
115
+ }
116
+
117
+ it("claims the window when the cluster really renders", () => {
118
+ stubTauriWindow();
119
+ const { claims, stop } = collect();
120
+ try {
121
+ const t = mountControls({ variant: "standard" });
122
+
123
+ expect(claims.map((c) => c.by)).toEqual(["HkWindowControls"]);
124
+ expect(document.documentElement.getAttribute("data-hk-chrome")).toBe("app");
125
+ t.app.unmount();
126
+ } finally {
127
+ stop();
128
+ cleanup();
129
+ }
130
+ });
131
+
132
+ it("does not claim when no variant was given (it renders nothing)", () => {
133
+ stubTauriWindow();
134
+ const { claims, stop } = collect();
135
+ try {
136
+ const t = mountControls({});
137
+ expect(claims).toHaveLength(0);
138
+ expect(document.documentElement.hasAttribute("data-hk-chrome")).toBe(false);
139
+ t.app.unmount();
140
+ } finally {
141
+ stop();
142
+ cleanup();
143
+ }
144
+ });
145
+
146
+ it("does not claim off-desktop (no Tauri window API)", () => {
147
+ const { claims, stop } = collect();
148
+ try {
149
+ const t = mountControls({ variant: "flush" });
150
+ expect(claims).toHaveLength(0);
151
+ expect(document.documentElement.hasAttribute("data-hk-chrome")).toBe(false);
152
+ t.app.unmount();
153
+ } finally {
154
+ stop();
155
+ cleanup();
156
+ }
157
+ });
158
+
159
+ it("does not claim on Apple platforms (the OS owns the traffic lights)", () => {
160
+ stubTauriWindow();
161
+ vi.stubGlobal("navigator", {
162
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
163
+ });
164
+ const { claims, stop } = collect();
165
+ try {
166
+ const t = mountControls({ variant: "standard" });
167
+ expect(claims).toHaveLength(0);
168
+ expect(document.documentElement.hasAttribute("data-hk-chrome")).toBe(false);
169
+ t.app.unmount();
170
+ } finally {
171
+ stop();
172
+ cleanup();
173
+ }
174
+ });
175
+
176
+ it("renders and claims when only __TAURI_INTERNALS__ exists (late __TAURI__)", async () => {
177
+ // The 2026-10-28 report: the documented API was late/absent at boot. The
178
+ // cluster must still paint three real controls and hand the window over,
179
+ // otherwise the boot bar never retires and two caption strips overlap.
180
+ const invoke = vi.fn(() => Promise.resolve(false));
181
+ vi.stubGlobal("__TAURI_INTERNALS__", {
182
+ invoke,
183
+ metadata: { currentWindow: { label: "main" } },
184
+ });
185
+ const { claims, stop } = collect();
186
+ try {
187
+ const t = mountControls({ variant: "standard" });
188
+ const buttons = t.container.querySelectorAll<HTMLButtonElement>(
189
+ ".hk-window-controls__btn",
190
+ );
191
+ expect(buttons).toHaveLength(3);
192
+ expect(claims.map((c) => c.by)).toEqual(["HkWindowControls"]);
193
+ expect(document.documentElement.getAttribute("data-hk-chrome")).toBe("app");
194
+
195
+ buttons[0]!.click();
196
+ buttons[1]!.click();
197
+ buttons[2]!.click();
198
+ await Promise.resolve();
199
+
200
+ expect(invoke).toHaveBeenCalledWith(WINDOW_IPC.minimize, {});
201
+ expect(invoke).toHaveBeenCalledWith(WINDOW_IPC.toggleMaximize, {});
202
+ expect(invoke).toHaveBeenCalledWith(WINDOW_IPC.close, {});
203
+ t.app.unmount();
204
+ } finally {
205
+ stop();
206
+ cleanup();
207
+ }
208
+ });
209
+
210
+ it("swallows a rejected is_maximized (no unhandled rejection during boot)", async () => {
211
+ // S2-6: the family's res/fatal-fallback.js raises the full-screen
212
+ // "Application failed to load" landing on `unhandledrejection` — during
213
+ // boot that is worse than a missing glyph, and the internals path polls,
214
+ // so an unhandled rejection would repeat every 500ms.
215
+ const invoke = vi.fn((command: string) =>
216
+ command === WINDOW_IPC.isMaximized
217
+ ? Promise.reject(new Error("window.is_maximized not allowed"))
218
+ : Promise.resolve(false),
219
+ );
220
+ vi.stubGlobal("__TAURI_INTERNALS__", { invoke });
221
+
222
+ const unhandled: unknown[] = [];
223
+ const onUnhandled = (reason: unknown) => void unhandled.push(reason);
224
+ process.on("unhandledRejection", onUnhandled);
225
+ const t = mountControls({ variant: "standard" });
226
+ try {
227
+ await new Promise((resolve) => setTimeout(resolve, 20));
228
+ // The cluster still renders and the rejection never escaped.
229
+ expect(
230
+ t.container.querySelectorAll(".hk-window-controls__btn"),
231
+ ).toHaveLength(3);
232
+ expect(unhandled).toHaveLength(0);
233
+ } finally {
234
+ t.app.unmount();
235
+ process.off("unhandledRejection", onUnhandled);
236
+ }
237
+ });
238
+
239
+ it("swallows rejected minimize/close (no unhandled rejection, no fake state)", async () => {
240
+ // S2-1 (R3): `void win.minimize()` without a rejection handler meant an
241
+ // ACL refusal became an unhandledrejection — which the family's
242
+ // res/fatal-fallback.js turns into the full-screen boot-failure landing —
243
+ // while the button kept looking perfectly usable.
244
+ const invoke = vi.fn((command: string) =>
245
+ command === WINDOW_IPC.isMaximized
246
+ ? Promise.resolve(false)
247
+ : Promise.reject(new Error(`${command} not allowed`)),
248
+ );
249
+ vi.stubGlobal("__TAURI_INTERNALS__", { invoke });
250
+
251
+ const unhandled: unknown[] = [];
252
+ const onUnhandled = (reason: unknown) => void unhandled.push(reason);
253
+ process.on("unhandledRejection", onUnhandled);
254
+ const t = mountControls({ variant: "standard" });
255
+ try {
256
+ const buttons = t.container.querySelectorAll<HTMLButtonElement>(
257
+ ".hk-window-controls__btn",
258
+ );
259
+ buttons[0]!.click(); // minimize
260
+ buttons[2]!.click(); // close
261
+ await new Promise((resolve) => setTimeout(resolve, 20));
262
+
263
+ expect(invoke).toHaveBeenCalledWith(WINDOW_IPC.minimize, {});
264
+ expect(invoke).toHaveBeenCalledWith(WINDOW_IPC.close, {});
265
+ expect(unhandled).toHaveLength(0);
266
+ } finally {
267
+ t.app.unmount();
268
+ process.off("unhandledRejection", onUnhandled);
269
+ }
270
+ });
271
+
272
+ it("stops the maximized poll when unmounted before onResized resolves (S2-7)", async () => {
273
+ vi.useFakeTimers();
274
+ try {
275
+ const invoke = vi.fn((_command: string) => Promise.resolve(false));
276
+ vi.stubGlobal("__TAURI_INTERNALS__", { invoke });
277
+
278
+ const t = mountControls({ variant: "standard" });
279
+ // Unmount while the awaited onResized() promise is still unresolved.
280
+ t.app.unmount();
281
+ await Promise.resolve();
282
+ await Promise.resolve();
283
+ await Promise.resolve();
284
+
285
+ const isMaximizedCalls = () =>
286
+ invoke.mock.calls.filter((call) => call[0] === WINDOW_IPC.isMaximized)
287
+ .length;
288
+ const afterUnmount = isMaximizedCalls();
289
+ vi.advanceTimersByTime(5000);
290
+ // An orphaned 500ms interval would keep polling a dead component.
291
+ expect(isMaximizedCalls()).toBe(afterUnmount);
292
+ } finally {
293
+ vi.useRealTimers();
294
+ }
295
+ });
296
+
297
+ it("claims once, not once per render", async () => {
298
+ // The maximized glyph re-renders the cluster; the takeover already happened.
299
+ vi.stubGlobal("__TAURI__", {
300
+ window: {
301
+ getCurrentWindow: () => ({
302
+ minimize: () => Promise.resolve(),
303
+ toggleMaximize: () => Promise.resolve(),
304
+ close: () => Promise.resolve(),
305
+ isMaximized: () => Promise.resolve(true),
306
+ onResized: () => Promise.resolve(() => {}),
307
+ }),
308
+ },
309
+ });
310
+ const { claims, stop } = collect();
311
+ try {
312
+ const t = mountControls({ variant: "standard" });
313
+ await nextTick();
314
+ await nextTick();
315
+ await nextTick();
316
+
317
+ // Proof the re-render happened: the maximize button now offers Restore.
318
+ expect(t.container.querySelector('[aria-label="Restore"]')).not.toBeNull();
319
+ expect(claims).toHaveLength(1);
320
+ t.app.unmount();
321
+ } finally {
322
+ stop();
323
+ cleanup();
324
+ }
325
+ });
326
+ });
@@ -2,6 +2,8 @@ import { defineComponent, onMounted, onUnmounted, ref, type PropType } from "vue
2
2
 
3
3
  import { Minus, Square, X, Copy } from "lucide-vue-next";
4
4
 
5
+ import { claimWindowChrome, resolveWindowApi } from "../runtime/windowChrome";
6
+
5
7
  import "./HkWindowControls.scss";
6
8
 
7
9
  /**
@@ -41,28 +43,15 @@ import "./HkWindowControls.scss";
41
43
  * renders nothing on Apple platforms and the header degrades to its
42
44
  * browser-face look automatically.
43
45
  *
44
- * The window API rides `__TAURI__.window.getCurrentWindow()` when
45
- * present; on a non-Tauri host (browser preview) the buttons render
46
- * but the calls no-op, keeping visual parity for storybook/tests.
46
+ * The window API comes from `resolveWindowApi()` (runtime/windowChrome):
47
+ * `__TAURI__.window.getCurrentWindow()` when the host exposes it, otherwise the
48
+ * `__TAURI_INTERNALS__` IPC surface. That fallback matters: the field report
49
+ * (2026-10-28) was a boot where `__TAURI__` was missing/late, this component
50
+ * rendered NOTHING, the boot bar never got claimed, and two caption strips
51
+ * fought over the top edge. The three gates below are unchanged; only the
52
+ * "is there a shell?" probe is shared now. No shell at all → nothing renders.
47
53
  */
48
54
 
49
- interface TauriWindowLike {
50
- minimize(): Promise<void>;
51
- toggleMaximize(): Promise<void>;
52
- close(): Promise<void>;
53
- isMaximized(): Promise<boolean>;
54
- onResized(cb: () => void): Promise<() => void>;
55
- }
56
-
57
- function currentWindow(): TauriWindowLike | null {
58
- const w = (
59
- window as unknown as {
60
- __TAURI__?: { window?: { getCurrentWindow?: () => TauriWindowLike } };
61
- }
62
- ).__TAURI__?.window;
63
- return w?.getCurrentWindow?.() ?? null;
64
- }
65
-
66
55
  /** Apple platforms own their traffic lights — we never render there. */
67
56
  export function isApplePlatform(): boolean {
68
57
  return /Mac|iPhone|iPad|iPod/.test(
@@ -86,14 +75,48 @@ export const HkWindowControls = defineComponent({
86
75
  setup(props) {
87
76
  const maximized = ref(false);
88
77
  let unlisten: (() => void) | null = null;
78
+ // Set on unmount: the mount path awaits `onResized()`, and an unmount that
79
+ // lands before that promise resolves would otherwise arm a poll interval
80
+ // (and keep writing `maximized`) for a component that no longer exists.
81
+ let disposed = false;
82
+ // Only a mount that actually paints the cluster takes the window chrome
83
+ // over from the boot bar (boot-chrome.js): the SAME gates the render
84
+ // function applies, replayed once on mount. A component that renders
85
+ // nothing must not claim — the boot bar would vanish and leave the window
86
+ // with no controls at all. On Apple platforms nothing is painted (the OS
87
+ // owns the traffic lights), so a host that ships only this component on
88
+ // macOS must dispatch `claimWindowChrome()` itself.
89
+ let claimed = false;
89
90
 
90
91
  onMounted(async () => {
91
- const win = currentWindow();
92
+ if (isApplePlatform()) return;
93
+ if (props.variant !== "standard" && props.variant !== "flush") return;
94
+ const win = resolveWindowApi();
92
95
  if (!win) return;
96
+ if (!claimed) {
97
+ claimed = true;
98
+ claimWindowChrome("HkWindowControls");
99
+ }
100
+ // Every shell read is failure-tolerant AND handled: an unhandled
101
+ // rejection during boot is treated as fatal by the family's
102
+ // `res/fatal-fallback.js` (it raises the full-screen "Application failed
103
+ // to load" landing on `unhandledrejection`), and the internals path would
104
+ // reproduce it every poll interval.
105
+ const refresh = () => {
106
+ void win.isMaximized().then(
107
+ (value) => {
108
+ if (!disposed) maximized.value = value;
109
+ },
110
+ () => {},
111
+ );
112
+ };
93
113
  try {
94
- const refresh = () => void win.isMaximized().then((m) => (maximized.value = m));
95
114
  refresh();
96
115
  const off = await win.onResized(refresh);
116
+ if (disposed) {
117
+ off();
118
+ return;
119
+ }
97
120
  unlisten = off;
98
121
  } catch {
99
122
  /* the window API is optional furniture */
@@ -101,7 +124,9 @@ export const HkWindowControls = defineComponent({
101
124
  });
102
125
 
103
126
  onUnmounted(() => {
127
+ disposed = true;
104
128
  unlisten?.();
129
+ unlisten = null;
105
130
  });
106
131
 
107
132
  return () => {
@@ -111,9 +136,26 @@ export const HkWindowControls = defineComponent({
111
136
  // enforce the required prop).
112
137
  if (isApplePlatform()) return null;
113
138
  if (props.variant !== "standard" && props.variant !== "flush") return null;
114
- const win = currentWindow();
139
+ const win = resolveWindowApi();
115
140
  if (!win) return null;
116
141
 
142
+ // Toggle, then re-read the shell state: the glyph must follow the real
143
+ // window, not a local guess (the internals path may have no resize event).
144
+ const toggleMaximize = () => {
145
+ void win
146
+ .toggleMaximize()
147
+ .then(() => win.isMaximized())
148
+ .then((value) => (maximized.value = value))
149
+ .catch(() => {});
150
+ };
151
+
152
+ // A refused shell call must never surface as an unhandled rejection (the
153
+ // family's res/fatal-fallback.js turns one into a full-screen boot
154
+ // failure) and must never look like a working button. The boot bar shows
155
+ // the degraded state when it is the one drawing the controls; here the
156
+ // host's own footer simply keeps the glyph it had.
157
+ const ignore = (pending: Promise<unknown>) => void pending.catch(() => {});
158
+
117
159
  const flush = props.variant === "flush";
118
160
  return (
119
161
  <div
@@ -124,7 +166,7 @@ export const HkWindowControls = defineComponent({
124
166
  type="button"
125
167
  class="hk-window-controls__btn"
126
168
  aria-label="Minimize"
127
- onClick={() => void win.minimize()}
169
+ onClick={() => ignore(win.minimize())}
128
170
  >
129
171
  <Minus size={flush ? 14 : 16} />
130
172
  </button>
@@ -132,7 +174,7 @@ export const HkWindowControls = defineComponent({
132
174
  type="button"
133
175
  class="hk-window-controls__btn"
134
176
  aria-label={maximized.value ? "Restore" : "Maximize"}
135
- onClick={() => void win.toggleMaximize()}
177
+ onClick={toggleMaximize}
136
178
  >
137
179
  {maximized.value
138
180
  ? <Copy size={flush ? 12 : 14} style={{ transform: "rotate(180deg)" }} />
@@ -142,7 +184,7 @@ export const HkWindowControls = defineComponent({
142
184
  type="button"
143
185
  class="hk-window-controls__btn hk-window-controls__btn--close"
144
186
  aria-label="Close"
145
- onClick={() => void win.close()}
187
+ onClick={() => ignore(win.close())}
146
188
  >
147
189
  <X size={flush ? 14 : 16} />
148
190
  </button>
package/src/index.ts CHANGED
@@ -826,3 +826,29 @@ export { type HAboutComponentVersion as HkAboutComponentVersion } from "./compon
826
826
  export { type HCustomTheme as HkCustomTheme } from "./components/HkColorSchemeDialog";
827
827
  export { OVERLAY_ROOT_SELECTOR, OVERLAY_Z, ensureOverlayRoot } from "./overlay/overlayRoot";
828
828
  export { HK_PAD_VAR, hkPadStyle, type HkPad } from "./runtime/containerPad";
829
+
830
+ // Window-chrome handoff (2026-10-28): the boot bar hikari's Vite plugin draws
831
+ // before the app bundle exists hands the window over on `hk:chrome-claim`.
832
+ // `claimWindowChrome` is for hosts that paint their OWN title bar instead of
833
+ // HkTitleBar/HkWindowControls (both claim automatically).
834
+ export {
835
+ claimWindowChrome,
836
+ resolveWindowApi,
837
+ CHROME_CLAIM_EVENT,
838
+ CHROME_OWNER_ATTR,
839
+ WINDOW_IPC,
840
+ RESIZE_POLL_MS,
841
+ type WindowApiLike,
842
+ } from "./runtime/windowChrome";
843
+ // Pre-boot caption bar (`bootChromePlugin`) is NOT exported here. It ships from
844
+ // its own published subpath — `@celestia-island/hikari/plugins/boot-chrome` —
845
+ // resolved to a self-contained ESM module, and that is what a host imports in
846
+ // vite.config.ts:
847
+ // 1. this entry is what BROWSER bundles import; re-exporting the Node-side
848
+ // plugin from it makes every consumer's build pull the module, and Vite
849
+ // then emits `boot-chrome.js` as a duplicate 28.8 kB asset AND warns
850
+ // "node:fs has been externalized for browser compatibility" (measured with
851
+ // Vite 8; tree-shaking does not remove a statically-referenced asset), and
852
+ // 2. the `/plugins` barrel is TypeScript, which Node cannot load from inside
853
+ // node_modules (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`) — the
854
+ // dedicated subpath exists precisely to avoid that.
@@ -0,0 +1,41 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Types for `boot-chrome.mjs` — the self-contained, published Vite plugin entry
5
+ * (`@celestia-island/hikari/plugins/boot-chrome`).
6
+ *
7
+ * The implementation is plain ESM with static Node imports on purpose: a
8
+ * consumer loads it from `node_modules` while resolving `vite.config.*`, where
9
+ * TypeScript cannot be stripped and Vite's `configLoader: "runner"` tears the
10
+ * module runner down before plugin hooks run. Keep the runtime file dependency
11
+ * free apart from `node:fs` / `node:path` / `node:url`.
12
+ */
13
+
14
+ /** File name the bar is emitted and served under (output-dir root). */
15
+ export declare const BOOT_CHROME_SCRIPT: "hk-boot-chrome.js";
16
+
17
+ /** Root-absolute default URL the injected `<script src>` points at. */
18
+ export declare const BOOT_CHROME_URL: string;
19
+
20
+ export interface BootChromePluginOptions {
21
+ /** Application name on the left of the bar (default: `document.title`). */
22
+ title?: string;
23
+ /** Bar height in px; `boot-chrome.js` clamps it to 20..64 (default 32). */
24
+ height?: number;
25
+ /** Watchdog delay in ms before the reload affordance appears (default 20000). */
26
+ hangMs?: number;
27
+ /**
28
+ * Which HTML entries get the bar. Default: only entries whose basename is
29
+ * `index.html`. Multi-window shells MUST configure this — a transparent
30
+ * overlay window must never paint the caption bar.
31
+ *
32
+ * A `RegExp` is matched against the entry's file path; a `string[]` is
33
+ * matched against the basename or the path suffix (`"pages/index.html"`).
34
+ */
35
+ htmlFiles?: string[] | RegExp;
36
+ }
37
+
38
+ /** Vite plugin: inject `boot-chrome.js` into the app window's HTML entry. */
39
+ export declare function bootChromePlugin(
40
+ options?: BootChromePluginOptions,
41
+ ): Plugin;