@celestia-island/hikari 0.55.141 → 0.55.142

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.141",
3
+ "version": "0.55.142",
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",
@@ -126,8 +126,9 @@ export const HkConnectionStatus = defineComponent({
126
126
  compact={props.compact}
127
127
  >
128
128
  {/* Host-appended popover actions (e.g. a manual page
129
- refresh) ride the same actions row as the built-in
130
- retry button. */}
129
+ refresh) ride the status row's right edge — the only
130
+ recovery button in the popover since the built-in
131
+ labelled retry retired (2026-10-11). */}
131
132
  {{ actions: () => slots.actions?.() }}
132
133
  </HkStatusBar>
133
134
  {bs && (
@@ -422,7 +422,7 @@ describe("HkStatusBar", () => {
422
422
  }
423
423
  });
424
424
 
425
- it("shows a working reconnect button in the popover while disconnected (hint text retired)", async () => {
425
+ it("retires the built-in reconnect button; the hint text carries the retry", async () => {
426
426
  const onRetry = vi.fn();
427
427
  const container = mountBar({
428
428
  version: "1.2.3",
@@ -436,20 +436,62 @@ describe("HkStatusBar", () => {
436
436
  .dispatchEvent(new MouseEvent("mouseenter"));
437
437
  await nextTick();
438
438
  const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
439
- // The popover body teleports to <body>: the old italic "click to
440
- // retry" hint pointed at an element whose clicks never reached the
441
- // tag handler. The actions row carries a REAL button instead, and
442
- // the misleading hint is gone whenever a retry is wired.
443
- const row = panel.querySelector<HTMLElement>("[data-status-actions]")!;
444
- expect(row, "actions row renders").toBeTruthy();
445
- const button = row.querySelector<HTMLButtonElement>("button")!;
446
- expect(button.textContent).toContain("Reconnect now");
447
- // 2026-10-02 user direction: small buttons with a leading glyph.
448
- expect(button.className).toContain("hk-btn-xs");
449
- expect(button.querySelector(".hk-btn-icon svg"), "leading glyph renders").toBeTruthy();
450
- expect(panel.textContent).not.toContain("Click to retry");
451
- button.click();
452
- expect(onRetry, "popover button click retries").toHaveBeenCalledTimes(1);
439
+ // 2026-10-11 user direction: the popover must not stack a built-in
440
+ // "Reconnect now" beside the host's own refresh — the light itself
441
+ // retries on click/tap, so the popover keeps ONE recovery action
442
+ // (the host's, via the slot).
443
+ expect(panel.querySelector("[data-status-actions]")).toBeNull();
444
+ expect(panel.textContent).not.toContain("Reconnect now");
445
+ // The fallback hint is back for hosts without a slot action — but it
446
+ // is a REAL control now: the original hint was retired because its
447
+ // clicks went nowhere, so the text carries the retry itself.
448
+ const hint = panel.querySelector<HTMLElement>("[role='button']")!;
449
+ expect(hint.textContent).toContain("Click to retry");
450
+ hint.click();
451
+ expect(onRetry, "hint click retries").toHaveBeenCalledTimes(1);
452
+ // Keyboard parity: the hint is a role=button control, so Enter (and
453
+ // Space) carry the same retry.
454
+ hint.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
455
+ expect(onRetry, "Enter on the hint retries").toHaveBeenCalledTimes(2);
456
+ hint.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));
457
+ expect(onRetry, "Space on the hint retries").toHaveBeenCalledTimes(2 + 1);
458
+ hint.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
459
+ expect(onRetry, "other keys are inert").toHaveBeenCalledTimes(3);
460
+ });
461
+
462
+ it("hosts the actions cluster on the status row's right side, not a bottom hairline row", async () => {
463
+ const container = document.createElement("div");
464
+ document.body.appendChild(container);
465
+ const app = createApp({
466
+ render: () =>
467
+ h(HkStatusBar, {
468
+ version: "1.2.3",
469
+ connectionStatus: "disconnected",
470
+ connectionInfo: INFO,
471
+ onRetry: () => {},
472
+ }, {
473
+ actions: () => h("button", { class: "host-refresh" }, "Refresh page"),
474
+ }),
475
+ });
476
+ app.mount(container);
477
+ mounts.push({ app, container });
478
+ await nextTick();
479
+ container
480
+ .querySelector<HTMLElement>(".s-status-bar-tag")!
481
+ .dispatchEvent(new MouseEvent("mouseenter"));
482
+ await nextTick();
483
+ const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
484
+ const clusters = panel.querySelectorAll("[data-status-actions]");
485
+ expect(clusters.length, "exactly one actions cluster").toBe(1);
486
+ // The cluster lives INSIDE the row that carries the status text —
487
+ // status left, actions right, no extra vertical row.
488
+ const statusRow = clusters[0]!.closest("div")!;
489
+ expect(statusRow.textContent).toContain("Disconnected");
490
+ // The old bottom row announced itself with a hairline top border;
491
+ // nothing in the panel may carry it anymore.
492
+ const bordered = [...panel.querySelectorAll<HTMLElement>("*[style*='border']")]
493
+ .filter((el) => el.style.borderTop.includes("solid"));
494
+ expect(bordered, "no hairline actions row remains").toEqual([]);
453
495
  });
454
496
 
455
497
  it("keeps the fallback hint when no onRetry is wired", async () => {
@@ -517,27 +559,38 @@ describe("HkStatusBar", () => {
517
559
  expect(panel!.querySelector("[data-status-actions]")).toBeNull();
518
560
  });
519
561
 
520
- it("keeps the actions row available while connection info is still fetching", async () => {
521
- const onRetry = vi.fn();
522
- const container = mountBar({
523
- version: "1.2.3",
524
- connectionStatus: "disconnected",
525
- connectionInfo: null,
526
- onRetry,
562
+ it("keeps host actions available while connection info is still fetching", async () => {
563
+ const onRefresh = vi.fn();
564
+ const container = document.createElement("div");
565
+ document.body.appendChild(container);
566
+ const app = createApp({
567
+ render: () =>
568
+ h(HkStatusBar, {
569
+ version: "1.2.3",
570
+ connectionStatus: "disconnected",
571
+ connectionInfo: null,
572
+ }, {
573
+ actions: () => h("button", { class: "host-refresh", onClick: onRefresh }, "Refresh page"),
574
+ }),
527
575
  });
576
+ app.mount(container);
577
+ mounts.push({ app, container });
528
578
  await nextTick();
529
579
  container
530
580
  .querySelector<HTMLElement>(".s-status-bar-tag")!
531
581
  .dispatchEvent(new MouseEvent("mouseenter"));
532
582
  await nextTick();
533
583
  const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
534
- // The row must live OUTSIDE the info/fetching ternary: an outage that
535
- // drops connectionInfo to null is exactly when the retry button is
536
- // most needed.
537
- const button = panel.querySelector<HTMLButtonElement>("[data-status-actions] button");
538
- expect(button, "retry button renders without info").toBeTruthy();
539
- button!.click();
540
- expect(onRetry).toHaveBeenCalledTimes(1);
584
+ // The cluster must live OUTSIDE the info/fetching ternary: an outage
585
+ // that drops connectionInfo to null is exactly when the recovery
586
+ // action is most needed. It rides the fetching line's right edge.
587
+ const cluster = panel.querySelector<HTMLElement>("[data-status-actions]");
588
+ expect(cluster, "actions cluster renders without info").toBeTruthy();
589
+ // Same line as the fetching text: label left, actions right.
590
+ expect(cluster!.closest("div")!.textContent).toContain("Fetching connection info");
591
+ const button = cluster!.querySelector<HTMLButtonElement>("button")!;
592
+ button.click();
593
+ expect(onRefresh).toHaveBeenCalledTimes(1);
541
594
  });
542
595
 
543
596
  it("no actions row when the slot renders nothing (wrapper forwards an empty slot)", async () => {
@@ -5,7 +5,6 @@ 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";
9
8
  import type { HkConnectionInfo } from "./HkConnectionInfo";
10
9
  import { HkCountdownDigit } from "./HkCountdownDigit";
11
10
  import { HkPlaceholderMarquee } from "./HkPlaceholderMarquee";
@@ -382,22 +381,24 @@ export const HkStatusBar = defineComponent({
382
381
  : t("hikari::statusBar.disconnected", "Disconnected");
383
382
 
384
383
  const connecting = mode === "reconnecting" || mode === "connecting";
385
- // Actions row: the popover body teleports to <body>, so a click on
386
- // the "click to reconnect" hint text inside it never reached the
387
- // tag's own click handler — the popover now carries REAL buttons.
388
- // The built-in one retriggers the reconnect for any non-green
389
- // light; hosts append their own (e.g. a full manual refresh)
390
- // through the `actions` slot.
391
- const showRetryButton = mode !== "connected" && typeof props.onRetry === "function";
392
- // Gate the row on RENDERED content, not on the slot function's
384
+ // Recovery actions (2026-10-11 user direction): the old bottom
385
+ // hairline row stacked a built-in "Reconnect now" button beside the
386
+ // host's own refresh icon — on an outage the popover read as TWO
387
+ // buttons doing one job. The built-in labelled button is RETIRED:
388
+ // the traffic light itself already retries on click/tap, and hosts
389
+ // append their single recovery action (e.g. a manual page refresh)
390
+ // through the `actions` slot. The cluster now rides the FIRST
391
+ // popover row's right side — status text left, actions right — so
392
+ // no extra row of vertical space is spent on it.
393
+ // Gate the cluster on RENDERED content, not on the slot function's
393
394
  // existence: HkConnectionStatus forwards `actions` unconditionally,
394
- // so a function-existence gate would paint an empty hairline row in
395
- // the connected state for every host that passed no #actions.
395
+ // so a function-existence gate would paint an empty cluster in the
396
+ // connected state for every host that passed no #actions.
396
397
  const actionVnodes = slots.actions?.();
397
398
  const hasHostActions = Array.isArray(actionVnodes)
398
399
  ? actionVnodes.length > 0
399
400
  : Boolean(actionVnodes);
400
- const hasActionsRow = showRetryButton || hasHostActions;
401
+ const hasActionsRow = hasHostActions;
401
402
 
402
403
  const pv = hkMergeVersionDisplay(props.version, props.panelBuildHash);
403
404
  const ev = props.engineVersion;
@@ -494,6 +495,21 @@ export const HkStatusBar = defineComponent({
494
495
  {latency} ms
495
496
  </span>
496
497
  )}
498
+ {hasActionsRow && (
499
+ <span
500
+ data-status-actions
501
+ style={{
502
+ display: "inline-flex",
503
+ alignItems: "center",
504
+ gap: "8px",
505
+ // No latency chip to push against: the cluster
506
+ // still lands on the row's right edge.
507
+ marginLeft: latency !== null ? undefined : "auto",
508
+ }}
509
+ >
510
+ {actionVnodes}
511
+ </span>
512
+ )}
497
513
  </div>
498
514
  {connecting && attempt > 0 && (
499
515
  <div style={{ display: "flex", alignItems: "center", gap: "4px", color: "rgb(var(--color-warning))", fontSize: "0.6875rem", marginBottom: "4px" }}>
@@ -509,8 +525,21 @@ export const HkStatusBar = defineComponent({
509
525
  )}
510
526
  </div>
511
527
  )}
512
- {mode === "disconnected" && !showRetryButton && (
513
- <div style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7 }}>
528
+ {mode === "disconnected" && !hasActionsRow && (
529
+ // Fallback for hosts without a slot action — and a REAL
530
+ // control this time: the original hint was retired
531
+ // because its clicks went nowhere (the popover body
532
+ // teleports to <body>), so the text now carries the
533
+ // retry itself, echo guard included.
534
+ <div
535
+ role="button"
536
+ tabindex={0}
537
+ onClick={onRetryButtonClick}
538
+ onKeydown={(e: KeyboardEvent) => {
539
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRetryButtonClick(); }
540
+ }}
541
+ style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7, cursor: "pointer" }}
542
+ >
514
543
  {t("hikari::statusBar.clickReconnect", "Click to retry")}
515
544
  </div>
516
545
  )}
@@ -565,31 +594,16 @@ export const HkStatusBar = defineComponent({
565
594
  ))}
566
595
  </>
567
596
  ) : (
568
- <div style={{ opacity: 0.5 }}>{t("hikari::statusBar.fetching", "Fetching connection info...")}</div>
569
- )}
570
- {hasActionsRow && (
571
- <div
572
- data-status-actions
573
- style={{
574
- display: "flex",
575
- alignItems: "center",
576
- gap: "8px",
577
- marginTop: "8px",
578
- paddingTop: "8px",
579
- borderTop: "1px solid var(--border-faint, rgb(var(--color-border) / 10%))",
580
- }}
581
- >
582
- {showRetryButton && (
583
- <HkButton
584
- size="xs"
585
- variant="secondary"
586
- icon="RotateCw"
587
- onClick={onRetryButtonClick}
588
- >
589
- {t("hikari::statusBar.retryNow", "Reconnect now")}
590
- </HkButton>
597
+ <div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
598
+ <span style={{ opacity: 0.5 }}>{t("hikari::statusBar.fetching", "Fetching connection info...")}</span>
599
+ {hasActionsRow && (
600
+ // An outage that drops connectionInfo to null is exactly
601
+ // when the recovery action is most needed — the cluster
602
+ // rides the fetching line's right edge.
603
+ <span data-status-actions style={{ display: "inline-flex", alignItems: "center", gap: "8px", marginLeft: "auto" }}>
604
+ {actionVnodes}
605
+ </span>
591
606
  )}
592
- {hasHostActions ? actionVnodes : null}
593
607
  </div>
594
608
  )}
595
609
  </div>
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "غير معروف",
152
152
  "hikari::statusBar.backend.warn": "متدهور",
153
153
  "hikari::statusBar.clickReconnect": "انقر لإعادة المحاولة",
154
- "hikari::statusBar.retryNow": "أعد الاتصال الآن",
155
154
  "hikari::statusBar.connected": "متصل",
156
155
  "hikari::statusBar.connecting": "جارٍ الاتصال...",
157
156
  "hikari::statusBar.disconnected": "غير متصل",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Unbekannt",
152
152
  "hikari::statusBar.backend.warn": "Beeinträchtigt",
153
153
  "hikari::statusBar.clickReconnect": "Zum Wiederholen klicken",
154
- "hikari::statusBar.retryNow": "Jetzt neu verbinden",
155
154
  "hikari::statusBar.connected": "Verbunden",
156
155
  "hikari::statusBar.connecting": "Verbinde...",
157
156
  "hikari::statusBar.disconnected": "Getrennt",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Unknown",
152
152
  "hikari::statusBar.backend.warn": "Degraded",
153
153
  "hikari::statusBar.clickReconnect": "Click to retry",
154
- "hikari::statusBar.retryNow": "Reconnect now",
155
154
  "hikari::statusBar.connected": "Connected",
156
155
  "hikari::statusBar.connecting": "Connecting...",
157
156
  "hikari::statusBar.disconnected": "Disconnected",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Desconocido",
152
152
  "hikari::statusBar.backend.warn": "Degradado",
153
153
  "hikari::statusBar.clickReconnect": "Haga clic para reintentar",
154
- "hikari::statusBar.retryNow": "Reconectar ahora",
155
154
  "hikari::statusBar.connected": "Conectado",
156
155
  "hikari::statusBar.connecting": "Conectando...",
157
156
  "hikari::statusBar.disconnected": "Desconectado",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Inconnu",
152
152
  "hikari::statusBar.backend.warn": "Dégradé",
153
153
  "hikari::statusBar.clickReconnect": "Cliquez pour réessayer",
154
- "hikari::statusBar.retryNow": "Reconnecter maintenant",
155
154
  "hikari::statusBar.connected": "Connecté",
156
155
  "hikari::statusBar.connecting": "Connexion...",
157
156
  "hikari::statusBar.disconnected": "Déconnecté",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "不明",
152
152
  "hikari::statusBar.backend.warn": "低下",
153
153
  "hikari::statusBar.clickReconnect": "クリックして再接続",
154
- "hikari::statusBar.retryNow": "今すぐ再接続",
155
154
  "hikari::statusBar.connected": "接続済み",
156
155
  "hikari::statusBar.connecting": "接続中...",
157
156
  "hikari::statusBar.disconnected": "切断",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "알 수 없음",
152
152
  "hikari::statusBar.backend.warn": "저하",
153
153
  "hikari::statusBar.clickReconnect": "클릭하여 재연결",
154
- "hikari::statusBar.retryNow": "지금 재연결",
155
154
  "hikari::statusBar.connected": "연결됨",
156
155
  "hikari::statusBar.connecting": "연결 중...",
157
156
  "hikari::statusBar.disconnected": "연결 끊김",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Desconhecido",
152
152
  "hikari::statusBar.backend.warn": "Degradado",
153
153
  "hikari::statusBar.clickReconnect": "Clique para tentar novamente",
154
- "hikari::statusBar.retryNow": "Reconectar agora",
155
154
  "hikari::statusBar.connected": "Conectado",
156
155
  "hikari::statusBar.connecting": "Conectando...",
157
156
  "hikari::statusBar.disconnected": "Desconectado",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "Неизвестно",
152
152
  "hikari::statusBar.backend.warn": "Деградация",
153
153
  "hikari::statusBar.clickReconnect": "Нажмите, чтобы повторить",
154
- "hikari::statusBar.retryNow": "Переподключиться сейчас",
155
154
  "hikari::statusBar.connected": "Подключено",
156
155
  "hikari::statusBar.connecting": "Подключение...",
157
156
  "hikari::statusBar.disconnected": "Отключено",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "未知",
152
152
  "hikari::statusBar.backend.warn": "降级",
153
153
  "hikari::statusBar.clickReconnect": "点击以尝试重连",
154
- "hikari::statusBar.retryNow": "立即重连",
155
154
  "hikari::statusBar.connected": "已连接",
156
155
  "hikari::statusBar.connecting": "连接中...",
157
156
  "hikari::statusBar.disconnected": "断开",
@@ -151,7 +151,6 @@
151
151
  "hikari::statusBar.backend.unknown": "未知",
152
152
  "hikari::statusBar.backend.warn": "降級",
153
153
  "hikari::statusBar.clickReconnect": "點擊以嘗試重連",
154
- "hikari::statusBar.retryNow": "立即重連",
155
154
  "hikari::statusBar.connected": "已連線",
156
155
  "hikari::statusBar.connecting": "連線中...",
157
156
  "hikari::statusBar.disconnected": "斷開",