@celestia-island/hikari 0.55.104 → 0.55.106

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.104",
3
+ "version": "0.55.106",
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",
@@ -45,6 +45,13 @@
45
45
  }
46
46
 
47
47
  .hk-board3d-fallback {
48
+ // Self-carried contrast chip: in transparent-backdrop mode the root
49
+ // paints nothing, so the note cannot lean on the old surface fill.
50
+ background: rgb(var(--color-surface) / 0.9);
51
+ border-radius: var(--radius-md, 8px);
52
+ padding: 6px 12px;
53
+ margin: auto;
54
+ width: fit-content;
48
55
  position: absolute;
49
56
  inset: 0;
50
57
  display: flex;
@@ -1376,4 +1376,25 @@ describe("HkBoard3D transparent background", () => {
1376
1376
  expect(rendererOf().alpha).toBe(true);
1377
1377
  expect(rendererOf().clearAlpha).toBe(0);
1378
1378
  });
1379
+
1380
+ it("drops the container's own surface paint in transparent mode", async () => {
1381
+ const { container } = mountBoard(
1382
+ { minimap: false, transparentBackground: true },
1383
+ {},
1384
+ );
1385
+ await nextTick();
1386
+ const root = container.querySelector(".hk-board3d") as HTMLElement;
1387
+ // The stylesheet paints --color-surface on .hk-board3d; an inline
1388
+ // transparent style must win, or a page-colored rectangle sits
1389
+ // behind the alpha-0 canvas (user report 2026-10-01: the scene
1390
+ // background differs from the page background).
1391
+ expect(root.style.background).toContain("transparent");
1392
+ });
1393
+
1394
+ it("keeps the stylesheet surface paint when the backdrop is opaque", async () => {
1395
+ const { container } = mountBoard({ minimap: false }, {});
1396
+ await nextTick();
1397
+ const root = container.querySelector(".hk-board3d") as HTMLElement;
1398
+ expect(root.style.background).toBe("");
1399
+ });
1379
1400
  });
@@ -1134,7 +1134,16 @@ export default defineComponent({
1134
1134
  });
1135
1135
 
1136
1136
  return () => (
1137
- <div class="hk-board3d" ref={containerRef}>
1137
+ <div
1138
+ class="hk-board3d"
1139
+ // The transparent-backdrop mode clears the CANVAS to alpha 0, but
1140
+ // the container's own surface paint would still sit behind it —
1141
+ // a page-colored rectangle where the page's backdrop should show.
1142
+ // An inline style wins over the stylesheet without a specificity
1143
+ // war with consumer classes.
1144
+ style={props.transparentBackground ? { background: "transparent" } : undefined}
1145
+ ref={containerRef}
1146
+ >
1138
1147
  <canvas ref={canvasRef} class="hk-board3d-canvas" />
1139
1148
  {failed.value ? (
1140
1149
  slots.fallback ? (
@@ -124,7 +124,12 @@ export const HkConnectionStatus = defineComponent({
124
124
  attemptNumber={probe.value.attemptNumber}
125
125
  countdown={probe.value.countdown}
126
126
  compact={props.compact}
127
- />
127
+ >
128
+ {/* Host-appended popover actions (e.g. a manual page
129
+ refresh) ride the same actions row as the built-in
130
+ retry button. */}
131
+ {{ actions: () => slots.actions?.() }}
132
+ </HkStatusBar>
128
133
  {bs && (
129
134
  <span class="s-status-bar-tag" style={{ cursor: "default" }}>
130
135
  <span
@@ -322,4 +322,145 @@ describe("HkStatusBar", () => {
322
322
  vi.useRealTimers();
323
323
  }
324
324
  });
325
+
326
+ it("shows a working reconnect button in the popover while disconnected (hint text retired)", async () => {
327
+ const onRetry = vi.fn();
328
+ const container = mountBar({
329
+ version: "1.2.3",
330
+ connectionStatus: "disconnected",
331
+ connectionInfo: INFO,
332
+ onRetry,
333
+ });
334
+ await nextTick();
335
+ container
336
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
337
+ .dispatchEvent(new MouseEvent("mouseenter"));
338
+ await nextTick();
339
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
340
+ // The popover body teleports to <body>: the old italic "click to
341
+ // retry" hint pointed at an element whose clicks never reached the
342
+ // tag handler. The actions row carries a REAL button instead, and
343
+ // the misleading hint is gone whenever a retry is wired.
344
+ const row = panel.querySelector<HTMLElement>("[data-status-actions]")!;
345
+ expect(row, "actions row renders").toBeTruthy();
346
+ const button = row.querySelector<HTMLButtonElement>("button")!;
347
+ expect(button.textContent).toContain("Reconnect now");
348
+ expect(panel.textContent).not.toContain("Click to retry");
349
+ button.click();
350
+ expect(onRetry, "popover button click retries").toHaveBeenCalledTimes(1);
351
+ });
352
+
353
+ it("keeps the fallback hint when no onRetry is wired", async () => {
354
+ const container = mountBar({
355
+ version: "1.2.3",
356
+ connectionStatus: "disconnected",
357
+ connectionInfo: INFO,
358
+ });
359
+ await nextTick();
360
+ container
361
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
362
+ .dispatchEvent(new MouseEvent("mouseenter"));
363
+ await nextTick();
364
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
365
+ expect(panel.querySelector("[data-status-actions]")).toBeNull();
366
+ expect(panel.textContent).toContain("Click to retry");
367
+ });
368
+
369
+ it("renders host actions through the actions slot, connected or not", async () => {
370
+ for (const connectionStatus of ["connected", "disconnected"] as const) {
371
+ document.body.innerHTML = "";
372
+ const onRefresh = vi.fn();
373
+ const container = document.createElement("div");
374
+ document.body.appendChild(container);
375
+ const app = createApp({
376
+ render: () =>
377
+ h(HkStatusBar, {
378
+ version: "1.2.3",
379
+ connectionStatus,
380
+ connectionInfo: INFO,
381
+ onRetry: () => {},
382
+ }, {
383
+ actions: () => h("button", { class: "host-refresh", onClick: onRefresh }, "Refresh page"),
384
+ }),
385
+ });
386
+ app.mount(container);
387
+ mounts.push({ app, container });
388
+ await nextTick();
389
+ container
390
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
391
+ .dispatchEvent(new MouseEvent("mouseenter"));
392
+ await nextTick();
393
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
394
+ const host = panel.querySelector<HTMLButtonElement>(".host-refresh")!;
395
+ expect(host, `host action renders (${connectionStatus})`).toBeTruthy();
396
+ expect(panel.querySelector("[data-status-actions]")).toBeTruthy();
397
+ host.click();
398
+ expect(onRefresh).toHaveBeenCalledTimes(1);
399
+ }
400
+ });
401
+
402
+ it("no actions row when connected without host actions", async () => {
403
+ const container = mountBar({
404
+ version: "1.2.3",
405
+ connectionStatus: "connected",
406
+ connectionInfo: INFO,
407
+ onRetry: () => {},
408
+ });
409
+ await nextTick();
410
+ container
411
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
412
+ .dispatchEvent(new MouseEvent("mouseenter"));
413
+ await nextTick();
414
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel");
415
+ expect(panel!.querySelector("[data-status-actions]")).toBeNull();
416
+ });
417
+
418
+ it("keeps the actions row available while connection info is still fetching", async () => {
419
+ const onRetry = vi.fn();
420
+ const container = mountBar({
421
+ version: "1.2.3",
422
+ connectionStatus: "disconnected",
423
+ connectionInfo: null,
424
+ onRetry,
425
+ });
426
+ await nextTick();
427
+ container
428
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
429
+ .dispatchEvent(new MouseEvent("mouseenter"));
430
+ await nextTick();
431
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
432
+ // The row must live OUTSIDE the info/fetching ternary: an outage that
433
+ // drops connectionInfo to null is exactly when the retry button is
434
+ // most needed.
435
+ const button = panel.querySelector<HTMLButtonElement>("[data-status-actions] button");
436
+ expect(button, "retry button renders without info").toBeTruthy();
437
+ button!.click();
438
+ expect(onRetry).toHaveBeenCalledTimes(1);
439
+ });
440
+
441
+ it("no actions row when the slot renders nothing (wrapper forwards an empty slot)", async () => {
442
+ const container = document.createElement("div");
443
+ document.body.appendChild(container);
444
+ const app = createApp({
445
+ render: () =>
446
+ h(HkStatusBar, {
447
+ version: "1.2.3",
448
+ connectionStatus: "connected",
449
+ connectionInfo: INFO,
450
+ }, {
451
+ // HkConnectionStatus forwards `actions` unconditionally; an
452
+ // empty render must not paint a hairline-only row.
453
+ actions: () => [],
454
+ }),
455
+ });
456
+ app.mount(container);
457
+ mounts.push({ app, container });
458
+ await nextTick();
459
+ container
460
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
461
+ .dispatchEvent(new MouseEvent("mouseenter"));
462
+ await nextTick();
463
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel");
464
+ expect(panel!.querySelector("[data-status-actions]")).toBeNull();
465
+ });
325
466
  });
@@ -5,6 +5,7 @@ import { useI18n } from "../i18n/context";
5
5
  import { HIKARI_FONT_MONO } from "../theme/fontContext";
6
6
 
7
7
  import HPopover from "./HkPopover";
8
+ import HkButton from "./HkButton";
8
9
  import type { HkConnectionInfo } from "./HkConnectionInfo";
9
10
  import { HkCountdownDigit } from "./HkCountdownDigit";
10
11
 
@@ -112,7 +113,7 @@ export const HkStatusBar = defineComponent({
112
113
  default: undefined,
113
114
  },
114
115
  },
115
- setup(props) {
116
+ setup(props, { slots }) {
116
117
  const popupOpen = ref(false);
117
118
  const anchorRef = ref<HTMLElement | null>(null);
118
119
  let closeTimer: ReturnType<typeof setTimeout> | null = null;
@@ -197,6 +198,16 @@ export const HkStatusBar = defineComponent({
197
198
  }
198
199
  }
199
200
 
201
+ // Popover retry button: the echo guard applies here too — a late
202
+ // synthetic click following the touch activation that opened the
203
+ // popover must not double-fire the retry through the button.
204
+ function onRetryButtonClick() {
205
+ if (Date.now() < touchEchoGuardUntil) return;
206
+ if (props.connectionStatus !== "connected") {
207
+ props.onRetry?.();
208
+ }
209
+ }
210
+
200
211
  // ── Touch activation ────────────────────────────────────────────
201
212
  // Mobile tap-to-retry used to ride the SYNTHESIZED mouse chain
202
213
  // (touchend → mouseenter → click → mouseleave), which browsers may
@@ -268,6 +279,22 @@ export const HkStatusBar = defineComponent({
268
279
  : t("hikari::statusBar.disconnected", "Disconnected");
269
280
 
270
281
  const connecting = mode === "reconnecting" || mode === "connecting";
282
+ // Actions row: the popover body teleports to <body>, so a click on
283
+ // the "click to reconnect" hint text inside it never reached the
284
+ // tag's own click handler — the popover now carries REAL buttons.
285
+ // The built-in one retriggers the reconnect for any non-green
286
+ // light; hosts append their own (e.g. a full manual refresh)
287
+ // through the `actions` slot.
288
+ const showRetryButton = mode !== "connected" && typeof props.onRetry === "function";
289
+ // Gate the row on RENDERED content, not on the slot function's
290
+ // existence: HkConnectionStatus forwards `actions` unconditionally,
291
+ // so a function-existence gate would paint an empty hairline row in
292
+ // the connected state for every host that passed no #actions.
293
+ const actionVnodes = slots.actions?.();
294
+ const hasHostActions = Array.isArray(actionVnodes)
295
+ ? actionVnodes.length > 0
296
+ : Boolean(actionVnodes);
297
+ const hasActionsRow = showRetryButton || hasHostActions;
271
298
 
272
299
  const pv = fmtVer(props.version, props.panelBuildHash);
273
300
  const ev = props.engineVersion;
@@ -370,7 +397,7 @@ export const HkStatusBar = defineComponent({
370
397
  )}
371
398
  </div>
372
399
  )}
373
- {mode === "disconnected" && (
400
+ {mode === "disconnected" && !showRetryButton && (
374
401
  <div style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7 }}>
375
402
  {t("hikari::statusBar.clickReconnect", "Click to retry")}
376
403
  </div>
@@ -418,6 +445,26 @@ export const HkStatusBar = defineComponent({
418
445
  ) : (
419
446
  <div style={{ opacity: 0.5 }}>{t("hikari::statusBar.fetching", "Fetching connection info...")}</div>
420
447
  )}
448
+ {hasActionsRow && (
449
+ <div
450
+ data-status-actions
451
+ style={{
452
+ display: "flex",
453
+ alignItems: "center",
454
+ gap: "8px",
455
+ marginTop: "8px",
456
+ paddingTop: "8px",
457
+ borderTop: "1px solid var(--border-faint, rgb(var(--color-border) / 10%))",
458
+ }}
459
+ >
460
+ {showRetryButton && (
461
+ <HkButton size="sm" variant="secondary" onClick={onRetryButtonClick}>
462
+ {t("hikari::statusBar.retryNow", "Reconnect now")}
463
+ </HkButton>
464
+ )}
465
+ {hasHostActions ? actionVnodes : null}
466
+ </div>
467
+ )}
421
468
  </div>
422
469
  </HPopover>
423
470
  </>
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "غير معروف",
151
151
  "hikari::statusBar.backend.warn": "متدهور",
152
152
  "hikari::statusBar.clickReconnect": "انقر لإعادة المحاولة",
153
+ "hikari::statusBar.retryNow": "أعد الاتصال الآن",
153
154
  "hikari::statusBar.connected": "متصل",
154
155
  "hikari::statusBar.connecting": "جارٍ الاتصال...",
155
156
  "hikari::statusBar.disconnected": "غير متصل",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Unbekannt",
151
151
  "hikari::statusBar.backend.warn": "Beeinträchtigt",
152
152
  "hikari::statusBar.clickReconnect": "Zum Wiederholen klicken",
153
+ "hikari::statusBar.retryNow": "Jetzt neu verbinden",
153
154
  "hikari::statusBar.connected": "Verbunden",
154
155
  "hikari::statusBar.connecting": "Verbinde...",
155
156
  "hikari::statusBar.disconnected": "Getrennt",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Unknown",
151
151
  "hikari::statusBar.backend.warn": "Degraded",
152
152
  "hikari::statusBar.clickReconnect": "Click to retry",
153
+ "hikari::statusBar.retryNow": "Reconnect now",
153
154
  "hikari::statusBar.connected": "Connected",
154
155
  "hikari::statusBar.connecting": "Connecting...",
155
156
  "hikari::statusBar.disconnected": "Disconnected",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Desconocido",
151
151
  "hikari::statusBar.backend.warn": "Degradado",
152
152
  "hikari::statusBar.clickReconnect": "Haga clic para reintentar",
153
+ "hikari::statusBar.retryNow": "Reconectar ahora",
153
154
  "hikari::statusBar.connected": "Conectado",
154
155
  "hikari::statusBar.connecting": "Conectando...",
155
156
  "hikari::statusBar.disconnected": "Desconectado",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Inconnu",
151
151
  "hikari::statusBar.backend.warn": "Dégradé",
152
152
  "hikari::statusBar.clickReconnect": "Cliquez pour réessayer",
153
+ "hikari::statusBar.retryNow": "Reconnecter maintenant",
153
154
  "hikari::statusBar.connected": "Connecté",
154
155
  "hikari::statusBar.connecting": "Connexion...",
155
156
  "hikari::statusBar.disconnected": "Déconnecté",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "不明",
151
151
  "hikari::statusBar.backend.warn": "低下",
152
152
  "hikari::statusBar.clickReconnect": "クリックして再接続",
153
+ "hikari::statusBar.retryNow": "今すぐ再接続",
153
154
  "hikari::statusBar.connected": "接続済み",
154
155
  "hikari::statusBar.connecting": "接続中...",
155
156
  "hikari::statusBar.disconnected": "切断",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "알 수 없음",
151
151
  "hikari::statusBar.backend.warn": "저하",
152
152
  "hikari::statusBar.clickReconnect": "클릭하여 재연결",
153
+ "hikari::statusBar.retryNow": "지금 재연결",
153
154
  "hikari::statusBar.connected": "연결됨",
154
155
  "hikari::statusBar.connecting": "연결 중...",
155
156
  "hikari::statusBar.disconnected": "연결 끊김",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Desconhecido",
151
151
  "hikari::statusBar.backend.warn": "Degradado",
152
152
  "hikari::statusBar.clickReconnect": "Clique para tentar novamente",
153
+ "hikari::statusBar.retryNow": "Reconectar agora",
153
154
  "hikari::statusBar.connected": "Conectado",
154
155
  "hikari::statusBar.connecting": "Conectando...",
155
156
  "hikari::statusBar.disconnected": "Desconectado",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "Неизвестно",
151
151
  "hikari::statusBar.backend.warn": "Деградация",
152
152
  "hikari::statusBar.clickReconnect": "Нажмите, чтобы повторить",
153
+ "hikari::statusBar.retryNow": "Переподключиться сейчас",
153
154
  "hikari::statusBar.connected": "Подключено",
154
155
  "hikari::statusBar.connecting": "Подключение...",
155
156
  "hikari::statusBar.disconnected": "Отключено",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "未知",
151
151
  "hikari::statusBar.backend.warn": "降级",
152
152
  "hikari::statusBar.clickReconnect": "点击以尝试重连",
153
+ "hikari::statusBar.retryNow": "立即重连",
153
154
  "hikari::statusBar.connected": "已连接",
154
155
  "hikari::statusBar.connecting": "连接中...",
155
156
  "hikari::statusBar.disconnected": "断开",
@@ -150,6 +150,7 @@
150
150
  "hikari::statusBar.backend.unknown": "未知",
151
151
  "hikari::statusBar.backend.warn": "降級",
152
152
  "hikari::statusBar.clickReconnect": "點擊以嘗試重連",
153
+ "hikari::statusBar.retryNow": "立即重連",
153
154
  "hikari::statusBar.connected": "已連線",
154
155
  "hikari::statusBar.connecting": "連線中...",
155
156
  "hikari::statusBar.disconnected": "斷開",