@celestia-island/hikari 0.55.177 → 0.55.179
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/README.md +35 -0
- package/package.json +4 -2
- package/src/animation/registerAnimations.test.ts +1 -0
- package/src/animation/registerAnimations.ts +5 -0
- package/src/components/HkConnectionStatus.shell.test.tsx +68 -2
- package/src/components/HkConnectionStatus.tsx +17 -3
- package/src/components/HkStatusBar.test.tsx +104 -7
- package/src/components/HkStatusBar.tsx +141 -129
- package/src/i18n/locales/ar/components.json +2 -2
- package/src/i18n/locales/de/components.json +2 -2
- package/src/i18n/locales/en/components.json +2 -2
- package/src/i18n/locales/es/components.json +2 -2
- package/src/i18n/locales/fr/components.json +2 -2
- package/src/i18n/locales/ja/components.json +2 -2
- package/src/i18n/locales/ko/components.json +2 -2
- package/src/i18n/locales/pt/components.json +2 -2
- package/src/i18n/locales/ru/components.json +2 -2
- package/src/i18n/locales/zh-Hans/components.json +2 -2
- package/src/i18n/locales/zh-Hant/components.json +2 -2
- package/src/styles/admin-tokens.scss +24 -0
package/README.md
CHANGED
|
@@ -9,6 +9,31 @@ npm install @celestia-island/hikari
|
|
|
9
9
|
pnpm add @celestia-island/hikari
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Build Requirements
|
|
13
|
+
|
|
14
|
+
This package is a **source distribution**: the tarball ships the TypeScript,
|
|
15
|
+
TSX and SCSS sources under `src/`, and your build pipeline compiles them like
|
|
16
|
+
first-party code. Nothing is pre-bundled, so consuming projects must provide:
|
|
17
|
+
|
|
18
|
+
| Requirement | Why |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `vue` (peer dependency) | Components are written for Vue 3 and import Vue at runtime. |
|
|
21
|
+
| TypeScript-aware bundler | Component and composable entry points resolve to `.ts`/`.tsx` sources (`main`/`types` → `src/index.ts`). |
|
|
22
|
+
| Vue JSX support | Components are written as `.tsx` render functions, not SFCs — enable `@vitejs/plugin-vue-jsx` (or your bundler's equivalent). |
|
|
23
|
+
| `sass` in your toolchain | Styling is distributed as SCSS; `@celestia-island/hikari/styles` resolves to an `.scss` aggregate your bundler compiles. Install `sass` as a devDependency of the consuming project. |
|
|
24
|
+
|
|
25
|
+
Minimal Vite configuration:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// vite.config.ts
|
|
29
|
+
import vue from '@vitejs/plugin-vue'
|
|
30
|
+
import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
plugins: [vue(), vueJsx()],
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
12
37
|
## Usage
|
|
13
38
|
|
|
14
39
|
Import styles once in your app entry point:
|
|
@@ -49,6 +74,16 @@ import { HkButton, HkCard, HkInput } from "@celestia-island/hikari";
|
|
|
49
74
|
- **HkAvatar** — image/fallback avatar
|
|
50
75
|
- And more...
|
|
51
76
|
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
`packages/vue` is its own pnpm workspace. After `corepack pnpm install`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm test # Vitest suite (colocated *.test.ts(x), happy-dom)
|
|
83
|
+
pnpm test:watch # Vitest in watch mode
|
|
84
|
+
pnpm lint # vue-tsc --noEmit type check
|
|
85
|
+
```
|
|
86
|
+
|
|
52
87
|
## License
|
|
53
88
|
|
|
54
89
|
Licensed under the [Synthetic Source License (SySL), Version 1.0](../../LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celestia-island/hikari",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.179",
|
|
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",
|
|
@@ -47,7 +47,9 @@
|
|
|
47
47
|
"dev": "vite",
|
|
48
48
|
"build": "vue-tsc --noEmit",
|
|
49
49
|
"build:docs": "vite build",
|
|
50
|
-
"lint": "
|
|
50
|
+
"lint": "vue-tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
51
53
|
"prepublishOnly": "vue-tsc --noEmit"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
@@ -61,6 +61,7 @@ describe("registerAnimations", () => {
|
|
|
61
61
|
expect(byName.get("hk-status-pill-pulse")?.infinite).toBe(true);
|
|
62
62
|
expect(byName.get("hk-pwd-breathe")?.infinite).toBe(true);
|
|
63
63
|
expect(byName.get("s-nav-item-badge-pulse")?.infinite).toBe(true);
|
|
64
|
+
expect(byName.get("s-status-bar-reconnecting-breathe")?.infinite).toBe(true);
|
|
64
65
|
expect(byName.get("s-auth-card-in")?.infinite).toBe(false);
|
|
65
66
|
expect(byName.get("hk-rolling-number-up")?.infinite).toBe(false);
|
|
66
67
|
expect(byName.get("hk-modal-breadcrumb-in")?.infinite).toBe(false);
|
|
@@ -70,6 +70,11 @@ registerCssAnimation("s-auth-card-in");
|
|
|
70
70
|
// the traffic light's green background blink when a dropped connection
|
|
71
71
|
// comes back.
|
|
72
72
|
registerCssAnimation("s-status-bar-recover-flash");
|
|
73
|
+
// styles/admin-tokens.scss — status-bar in-flight breathe (loops
|
|
74
|
+
// forever): the connecting/reconnecting light's warning tint pulse —
|
|
75
|
+
// a fixed background means a settled state (the gray idle), an
|
|
76
|
+
// in-flight one visibly breathes (2026-10-17 user direction).
|
|
77
|
+
registerCssAnimation("s-status-bar-reconnecting-breathe", { infinite: true });
|
|
73
78
|
// components/HkStatusPill.scss — live-dot pulse (loops forever).
|
|
74
79
|
registerCssAnimation("hk-status-pill-pulse", { infinite: true });
|
|
75
80
|
// components/HkRollingNumber.scss — digit roll-up (plays once, forwards).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createApp, h } from "vue";
|
|
2
|
+
import { createApp, h, nextTick, ref } from "vue";
|
|
3
3
|
|
|
4
|
-
import { HkConnectionStatus } from "./HkConnectionStatus";
|
|
4
|
+
import { HkConnectionStatus, HK_CONNECTION_PROBE } from "./HkConnectionStatus";
|
|
5
5
|
|
|
6
6
|
/** The shell contract: WITHOUT injections the component never touches
|
|
7
7
|
* the network — the fetch-poll was demoted with the chat kit (the M1
|
|
@@ -31,4 +31,70 @@ describe("HkConnectionStatus (shell)", () => {
|
|
|
31
31
|
globalThis.fetch = holder;
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
it("idle pins the light to the resting gray even when the probe reports a fault", () => {
|
|
36
|
+
// 2026-10-17 user direction: sessionless auth surfaces pin the light
|
|
37
|
+
// to idle — "not yet logged in" is not a fault — through THIS one
|
|
38
|
+
// wiring, not a hand-rolled HkStatusBar fork (the fork was what left
|
|
39
|
+
// the auth popover stuck on its fetching placeholder with no
|
|
40
|
+
// versions). The probe underneath reports red; the override wins.
|
|
41
|
+
const probe = {
|
|
42
|
+
result: ref({
|
|
43
|
+
connected: false,
|
|
44
|
+
state: "disconnected" as const,
|
|
45
|
+
transportTier: "poll",
|
|
46
|
+
tier: "poll",
|
|
47
|
+
latencyMs: null,
|
|
48
|
+
attemptNumber: 1,
|
|
49
|
+
retryCount: 1,
|
|
50
|
+
retryTotal: 3,
|
|
51
|
+
countdown: 0,
|
|
52
|
+
}),
|
|
53
|
+
retryNow: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
const container = document.createElement("div");
|
|
56
|
+
document.body.appendChild(container);
|
|
57
|
+
const app = createApp({
|
|
58
|
+
render: () =>
|
|
59
|
+
h(HkConnectionStatus, { standalone: true, version: "1.2.3", idle: true, compact: true }),
|
|
60
|
+
});
|
|
61
|
+
app.provide(HK_CONNECTION_PROBE, probe);
|
|
62
|
+
app.mount(container);
|
|
63
|
+
mounts.push({ app, container });
|
|
64
|
+
const dot = container.querySelector<HTMLElement>(".s-status-bar-dot")!;
|
|
65
|
+
expect(dot, "light renders").toBeTruthy();
|
|
66
|
+
// happy-dom drops inline styles whose value carries var() fallbacks,
|
|
67
|
+
// so the gray pin rides the compact aria contract: the idle override
|
|
68
|
+
// turns a faulting probe into "Ready", never "Disconnected".
|
|
69
|
+
expect(container.querySelector<HTMLElement>(".s-status-bar-tag")!.getAttribute("aria-label"))
|
|
70
|
+
.toBe("Ready · 1.2.3");
|
|
71
|
+
// Idle is a settled resting state — clicking the gray light must not
|
|
72
|
+
// fire a silent probe retry (R3 catch).
|
|
73
|
+
container.querySelector<HTMLElement>(".s-status-bar-tag")!.click();
|
|
74
|
+
expect(probe.retryNow).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("the inert shell shows a definite status with versions but no probe rows", async () => {
|
|
78
|
+
// No injection: INERT_PROBE's sentinel transportTier is "unknown" —
|
|
79
|
+
// it must not leak a bare "Protocol unknown" row (which has no
|
|
80
|
+
// locale key in ANY language) into the popover (R3 catch). The card
|
|
81
|
+
// keeps the definite status heading + the known versions only.
|
|
82
|
+
const container = document.createElement("div");
|
|
83
|
+
document.body.appendChild(container);
|
|
84
|
+
const app = createApp({
|
|
85
|
+
render: () => h(HkConnectionStatus, { standalone: false, version: "1.2.3" }),
|
|
86
|
+
});
|
|
87
|
+
app.mount(container);
|
|
88
|
+
mounts.push({ app, container });
|
|
89
|
+
await nextTick();
|
|
90
|
+
container
|
|
91
|
+
.querySelector<HTMLElement>(".s-status-bar-tag")!
|
|
92
|
+
.dispatchEvent(new MouseEvent("mouseenter"));
|
|
93
|
+
await nextTick();
|
|
94
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
95
|
+
expect(panel.textContent).toContain("Disconnected");
|
|
96
|
+
expect(panel.textContent).toContain("1.2.3");
|
|
97
|
+
expect(panel.textContent).not.toContain("Protocol");
|
|
98
|
+
expect(panel.textContent).not.toContain("unknown");
|
|
99
|
+
});
|
|
34
100
|
});
|
|
@@ -65,8 +65,10 @@ const backendStateColor: Record<HkBackendStatus["state"], string> = {
|
|
|
65
65
|
* HK_CONNECTION_PROBE, that probe (and its retryNow) is used verbatim;
|
|
66
66
|
* without an injection the shell renders an inert unknown state and
|
|
67
67
|
* NEVER touches the network (pinned). Slots:
|
|
68
|
-
* - `left`
|
|
69
|
-
* - `right`
|
|
68
|
+
* - `left` — extras after the status tags (e.g. a mock badge)
|
|
69
|
+
* - `right` — extras on the far right (e.g. an engine version tag)
|
|
70
|
+
* - `actions` — forwarded to HkStatusBar's status-row cluster (e.g. the
|
|
71
|
+
* hosts' single manual page refresh, 2026-10-11 direction)
|
|
70
72
|
*/
|
|
71
73
|
export const HkConnectionStatus = defineComponent({
|
|
72
74
|
name: "HkConnectionStatus",
|
|
@@ -77,6 +79,15 @@ export const HkConnectionStatus = defineComponent({
|
|
|
77
79
|
standalone: { type: Boolean, default: true },
|
|
78
80
|
/** Forwarded to HkStatusBar — collapse to the traffic-light dot. */
|
|
79
81
|
compact: { type: Boolean, default: false },
|
|
82
|
+
/** Resting override (2026-10-17 user direction): pins the light and
|
|
83
|
+
* the status line to the neutral-gray idle state — "the shell is
|
|
84
|
+
* alive, no session yet" — regardless of what the injected probe
|
|
85
|
+
* reports. Sessionless auth surfaces set it; logged-in surfaces
|
|
86
|
+
* never do. Everything else (versions, info rows, actions) keeps
|
|
87
|
+
* the ONE wiring — idle hosts no longer hand-roll an HkStatusBar
|
|
88
|
+
* copy (that fork is what left the auth popover stuck on its
|
|
89
|
+
* fetching placeholder). */
|
|
90
|
+
idle: { type: Boolean, default: false },
|
|
80
91
|
},
|
|
81
92
|
setup(props, { slots }) {
|
|
82
93
|
// SHELL-ONLY: the network side (WHO polls /api/health and HOW)
|
|
@@ -119,7 +130,10 @@ export const HkConnectionStatus = defineComponent({
|
|
|
119
130
|
};
|
|
120
131
|
});
|
|
121
132
|
|
|
122
|
-
const connectionStatus = computed<"connected" | "reconnecting" | "disconnected">(() => {
|
|
133
|
+
const connectionStatus = computed<"connected" | "reconnecting" | "disconnected" | "idle">(() => {
|
|
134
|
+
// The idle override wins over the probe: the resting gray light is
|
|
135
|
+
// a definite statement about the SHELL, not a verdict on the link.
|
|
136
|
+
if (props.idle) return "idle";
|
|
123
137
|
const s = probe.value.state;
|
|
124
138
|
if (s === "connected") return "connected";
|
|
125
139
|
if (s === "connecting" || s === "reconnecting") return "reconnecting";
|
|
@@ -173,7 +173,9 @@ describe("HkStatusBar", () => {
|
|
|
173
173
|
const row = panel!.querySelector('[data-extra-detail="gateway"]');
|
|
174
174
|
expect(row).not.toBeNull();
|
|
175
175
|
expect((row as HTMLElement).style.maxWidth).toBe("400px");
|
|
176
|
-
|
|
176
|
+
// Scoped to the extra-detail row: the version cells (always in the
|
|
177
|
+
// popover since the 2026-10-17 wave) are title-bearing spans too.
|
|
178
|
+
const value = row!.querySelector("span[title]") as HTMLElement;
|
|
177
179
|
expect(value.textContent).toBe("gateway.celestia.world");
|
|
178
180
|
expect(value.title).toBe("gateway.celestia.world");
|
|
179
181
|
expect(value.style.textOverflow).toBe("ellipsis");
|
|
@@ -559,7 +561,12 @@ describe("HkStatusBar", () => {
|
|
|
559
561
|
expect(panel!.querySelector("[data-status-actions]")).toBeNull();
|
|
560
562
|
});
|
|
561
563
|
|
|
562
|
-
it("
|
|
564
|
+
it("without info the popover states a definite status with versions, never a fetching placeholder", async () => {
|
|
565
|
+
// 2026-10-17 user report: the idle auth pill's popover printed
|
|
566
|
+
// "Fetching connection info..." forever while its steady gray light
|
|
567
|
+
// meant a settled state. The info-null branch is RETIRED: the title
|
|
568
|
+
// row states the definite state and the known versions render up
|
|
569
|
+
// front; the recovery action stays on that title row.
|
|
563
570
|
const onRefresh = vi.fn();
|
|
564
571
|
const container = document.createElement("div");
|
|
565
572
|
document.body.appendChild(container);
|
|
@@ -581,18 +588,108 @@ describe("HkStatusBar", () => {
|
|
|
581
588
|
.dispatchEvent(new MouseEvent("mouseenter"));
|
|
582
589
|
await nextTick();
|
|
583
590
|
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
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
591
|
const cluster = panel.querySelector<HTMLElement>("[data-status-actions]");
|
|
588
592
|
expect(cluster, "actions cluster renders without info").toBeTruthy();
|
|
589
|
-
//
|
|
590
|
-
expect(cluster!.closest("div")!.textContent).toContain("
|
|
593
|
+
// The title row is the DEFINITE state, not a fetching placeholder.
|
|
594
|
+
expect(cluster!.closest("div")!.textContent).toContain("Disconnected");
|
|
595
|
+
expect(panel.textContent).not.toContain("Fetching connection info");
|
|
596
|
+
// Versions are known facts — up front, info or not.
|
|
597
|
+
expect(panel.querySelectorAll(".s-status-bar-popover-value").length).toBeGreaterThanOrEqual(1);
|
|
598
|
+
expect(panel.textContent).toContain("1.2.3");
|
|
599
|
+
// Without probe facts the protocol/network rows stay honest (absent).
|
|
600
|
+
expect(panel.textContent).not.toContain("Protocol");
|
|
591
601
|
const button = cluster!.querySelector<HTMLButtonElement>("button")!;
|
|
592
602
|
button.click();
|
|
593
603
|
expect(onRefresh).toHaveBeenCalledTimes(1);
|
|
594
604
|
});
|
|
595
605
|
|
|
606
|
+
it("renders the version rows in the popover regardless of compact", async () => {
|
|
607
|
+
// 2026-10-17 user direction: the popover is the details card — the
|
|
608
|
+
// versions (panel + engine, merged once) show there up front even in
|
|
609
|
+
// full (non-compact) mode, no longer gated behind `compact`.
|
|
610
|
+
const container = mountBar({
|
|
611
|
+
version: "1.2.3 master:49fe7ae",
|
|
612
|
+
panelBuildHash: "master:49fe7ae",
|
|
613
|
+
engineVersion: "9.8.7",
|
|
614
|
+
engineBuildHash: "engine:abcdef0",
|
|
615
|
+
connectionStatus: "connected",
|
|
616
|
+
connectionInfo: INFO,
|
|
617
|
+
});
|
|
618
|
+
await nextTick();
|
|
619
|
+
tagOf(container).dispatchEvent(new MouseEvent("mouseenter"));
|
|
620
|
+
await nextTick();
|
|
621
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
622
|
+
const cells = [...panel.querySelectorAll<HTMLElement>(".s-status-bar-popover-value .s-status-bar-version")]
|
|
623
|
+
.map((el) => el.querySelector(".s-status-bar-version__static")!.textContent ?? "");
|
|
624
|
+
// Set-merged once: the build hash the version line already names is
|
|
625
|
+
// not printed twice in the popover either.
|
|
626
|
+
expect(cells).toContain("1.2.3 master:49fe7ae");
|
|
627
|
+
expect(cells).toContain("9.8.7 engine:abcdef0");
|
|
628
|
+
expect(cells.filter((v) => v.includes("49fe7ae")).length).toBe(1);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("idle renders the resting gray light, the Ready title row, and no reconnecting pulse", async () => {
|
|
632
|
+
const container = mountBar({
|
|
633
|
+
version: "1.2.3",
|
|
634
|
+
connectionStatus: "idle",
|
|
635
|
+
connectionInfo: null,
|
|
636
|
+
});
|
|
637
|
+
await nextTick();
|
|
638
|
+
const tag = tagOf(container);
|
|
639
|
+
// Steady state: idle must NOT carry the in-flight warning pulse.
|
|
640
|
+
expect(tag.className).not.toContain("s-status-bar-tag-reconnecting");
|
|
641
|
+
// happy-dom drops inline styles whose value carries var() fallbacks,
|
|
642
|
+
// so the gray pin is a source-contract assertion: the idle entry in
|
|
643
|
+
// the dot color map must name the tertiary (resting gray) token —
|
|
644
|
+
// the same contract the 2026-10-17 user direction fixed (#815).
|
|
645
|
+
const source = readFileSync(resolve(__dirname, "./HkStatusBar.tsx"), "utf8");
|
|
646
|
+
expect(source).toMatch(/idle:\s*"rgb\(var\(--color-text-tertiary/);
|
|
647
|
+
// The popover states the definite Ready state (the hikari i18n
|
|
648
|
+
// fallback) with the version row, and offers no protocol/network
|
|
649
|
+
// facts the idle shell does not have.
|
|
650
|
+
tag.dispatchEvent(new MouseEvent("mouseenter"));
|
|
651
|
+
await nextTick();
|
|
652
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
653
|
+
expect(panel.textContent).toContain("Ready");
|
|
654
|
+
expect(panel.textContent).toContain("1.2.3");
|
|
655
|
+
expect(panel.textContent).not.toContain("Protocol");
|
|
656
|
+
// Idle is a SETTLED state — it must not offer the disconnected-only
|
|
657
|
+
// "Click to retry" hint (R1 mutation catch: the gate survived, the
|
|
658
|
+
// pin was missing).
|
|
659
|
+
expect(panel.textContent).not.toContain("Click to retry");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it("pins the in-flight breathe pulse contract in admin-tokens.scss", () => {
|
|
663
|
+
// 2026-10-17 user direction: a genuinely in-flight (connecting/
|
|
664
|
+
// reconnecting) light must visibly breathe — a FIXED background
|
|
665
|
+
// means a settled state, which is the gray idle's job now.
|
|
666
|
+
// Reduced-motion users keep the steady tint.
|
|
667
|
+
const scss = readFileSync(resolve(__dirname, "../styles/admin-tokens.scss"), "utf8");
|
|
668
|
+
const rule = /\.s-status-bar-tag-reconnecting\s*\{[^}]*\}/.exec(scss)?.[0] ?? "";
|
|
669
|
+
expect(rule, "reconnecting rule carries the breathe animation").toContain("animation:");
|
|
670
|
+
expect(scss).toContain("@keyframes s-status-bar-reconnecting-breathe");
|
|
671
|
+
expect(scss).toMatch(
|
|
672
|
+
/@media[^{]*prefers-reduced-motion[^{]*\{[^@]*?\.s-status-bar-tag-reconnecting\s*\{[^}]*animation:\s*none/s,
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("connecting carries the in-flight pulse class; idle does not", async () => {
|
|
677
|
+
const connecting = mountBar({
|
|
678
|
+
version: "1.2.3",
|
|
679
|
+
connectionStatus: "connecting",
|
|
680
|
+
connectionInfo: null,
|
|
681
|
+
});
|
|
682
|
+
await nextTick();
|
|
683
|
+
expect(tagOf(connecting).className).toContain("s-status-bar-tag-reconnecting");
|
|
684
|
+
const idle = mountBar({
|
|
685
|
+
version: "1.2.3",
|
|
686
|
+
connectionStatus: "idle",
|
|
687
|
+
connectionInfo: null,
|
|
688
|
+
});
|
|
689
|
+
await nextTick();
|
|
690
|
+
expect(tagOf(idle).className).not.toContain("s-status-bar-tag-reconnecting");
|
|
691
|
+
});
|
|
692
|
+
|
|
596
693
|
it("no actions row when the slot renders nothing (wrapper forwards an empty slot)", async () => {
|
|
597
694
|
const container = document.createElement("div");
|
|
598
695
|
document.body.appendChild(container);
|
|
@@ -14,8 +14,13 @@ import { HkPlaceholderMarquee } from "./HkPlaceholderMarquee";
|
|
|
14
14
|
* (Upstreamed from shittim-chest's plana-legacy layer.)
|
|
15
15
|
*
|
|
16
16
|
* A traffic-light dot plus a two-column-grid version block (panel row,
|
|
17
|
-
* engine row). Hovering/tapping the pill opens an HPopover
|
|
18
|
-
*
|
|
17
|
+
* engine row). Hovering/tapping the pill opens an HPopover whose title
|
|
18
|
+
* row is ALWAYS a definite state (Ready/Connected/Connecting/Disconnected)
|
|
19
|
+
* followed by the version rows — both render with or without probe facts
|
|
20
|
+
* (2026-10-17 user direction: the popover must never sit on a perpetual
|
|
21
|
+
* "fetching" placeholder, and the versions show up front). The quality
|
|
22
|
+
* icon, latency, retry countdown and protocol/network rows join once the
|
|
23
|
+
* probe supplies them.
|
|
19
24
|
* With `compact` the pill collapses to the BARE DOT only (mobile
|
|
20
25
|
* logged-in footers) — no inline status text next to the light; that
|
|
21
26
|
* label is a standing user directive (asked to be removed repeatedly,
|
|
@@ -180,7 +185,7 @@ export const HkStatusBar = defineComponent({
|
|
|
180
185
|
panelBuildHash: { type: String as PropType<string | undefined>, default: undefined },
|
|
181
186
|
engineBuildHash: { type: String as PropType<string | undefined>, default: undefined },
|
|
182
187
|
connectionStatus: {
|
|
183
|
-
type: String as PropType<"connected" | "reconnecting" | "disconnected" | "connecting">,
|
|
188
|
+
type: String as PropType<"connected" | "reconnecting" | "disconnected" | "connecting" | "idle">,
|
|
184
189
|
default: "disconnected",
|
|
185
190
|
},
|
|
186
191
|
connectionInfo: {
|
|
@@ -238,6 +243,10 @@ export const HkStatusBar = defineComponent({
|
|
|
238
243
|
connecting: "rgb(var(--color-warning))",
|
|
239
244
|
reconnecting: "rgb(var(--color-warning))",
|
|
240
245
|
disconnected: "rgb(var(--color-error))",
|
|
246
|
+
// Idle (2026-10-17 user direction): the shell is alive but has no
|
|
247
|
+
// session — the login page's resting light. Neutral gray, not red:
|
|
248
|
+
// red means FAULT, and "not yet logged in" is not a fault.
|
|
249
|
+
idle: "rgb(var(--color-text-tertiary, var(--color-text-secondary, 128 128 128)))",
|
|
241
250
|
};
|
|
242
251
|
|
|
243
252
|
// ── Recovery flash ─────────────────────────────────────────────
|
|
@@ -299,7 +308,10 @@ export const HkStatusBar = defineComponent({
|
|
|
299
308
|
let touchEchoGuardUntil = 0;
|
|
300
309
|
function onTagClick() {
|
|
301
310
|
if (Date.now() < touchEchoGuardUntil) return;
|
|
302
|
-
|
|
311
|
+
// Idle is a SETTLED resting state ("shell alive, no session"), not
|
|
312
|
+
// a fault — clicking the gray light must not fire a silent retry
|
|
313
|
+
// (R3 catch: it looked like a dead control doing invisible work).
|
|
314
|
+
if (props.connectionStatus !== "connected" && props.connectionStatus !== "idle") {
|
|
303
315
|
props.onRetry?.();
|
|
304
316
|
}
|
|
305
317
|
}
|
|
@@ -380,7 +392,8 @@ export const HkStatusBar = defineComponent({
|
|
|
380
392
|
const countdown = props.countdown ?? info?.countdown ?? 0;
|
|
381
393
|
|
|
382
394
|
const tierLabelKey = `hikari::statusBar.tier.${tier}`;
|
|
383
|
-
const statusText = mode === "
|
|
395
|
+
const statusText = mode === "idle" ? t("hikari::statusBar.idle", "Ready")
|
|
396
|
+
: mode === "connected" ? t("hikari::statusBar.connected", "Connected")
|
|
384
397
|
: mode === "reconnecting" || mode === "connecting" ? t("hikari::statusBar.connecting", "Connecting...")
|
|
385
398
|
: t("hikari::statusBar.disconnected", "Disconnected");
|
|
386
399
|
|
|
@@ -480,140 +493,139 @@ export const HkStatusBar = defineComponent({
|
|
|
480
493
|
// 300px default floor (2026-10-11 user direction: the
|
|
481
494
|
// version/protocol rows read cramped at the old 220px),
|
|
482
495
|
// clamped to the popover's own viewport budget so a
|
|
483
|
-
// 320px-class phone never spills past the
|
|
496
|
+
// 320px-class phone never spills past the viewport (the
|
|
497
|
+
// glass chrome eats a few px of the card's inner width
|
|
498
|
+
// at the very narrow end — cosmetic, bounded, R3-note).
|
|
484
499
|
minWidth: "min(300px, calc(100vw - 2 * var(--viewport-gutter, 16px)))",
|
|
485
500
|
padding: "10px 14px",
|
|
486
501
|
fontSize: "0.75rem", lineHeight: 1.6,
|
|
487
502
|
color: "rgb(var(--color-text))",
|
|
488
503
|
}}
|
|
489
504
|
>
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
// control this time: the original hint was retired
|
|
535
|
-
// because its clicks went nowhere (the popover body
|
|
536
|
-
// teleports to <body>), so the text now carries the
|
|
537
|
-
// retry itself, echo guard included.
|
|
538
|
-
<div
|
|
539
|
-
role="button"
|
|
540
|
-
tabindex={0}
|
|
541
|
-
onClick={onRetryButtonClick}
|
|
542
|
-
onKeydown={(e: KeyboardEvent) => {
|
|
543
|
-
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRetryButtonClick(); }
|
|
544
|
-
}}
|
|
545
|
-
style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7, cursor: "pointer" }}
|
|
546
|
-
>
|
|
547
|
-
{t("hikari::statusBar.clickReconnect", "Click to retry")}
|
|
548
|
-
</div>
|
|
549
|
-
)}
|
|
550
|
-
{props.compact && (
|
|
551
|
-
<>
|
|
552
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
553
|
-
<Monitor size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
554
|
-
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.panel", "Panel")}</span>
|
|
555
|
-
<span
|
|
556
|
-
class="s-status-bar-popover-value"
|
|
557
|
-
style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})` }}
|
|
558
|
-
>
|
|
559
|
-
<HkVersionValue text={pv} />
|
|
560
|
-
</span>
|
|
561
|
-
</div>
|
|
562
|
-
{ev && (
|
|
563
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
564
|
-
<Cog size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
565
|
-
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.engine", "Engine")}</span>
|
|
566
|
-
<span
|
|
567
|
-
class="s-status-bar-popover-value"
|
|
568
|
-
style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})` }}
|
|
569
|
-
>
|
|
570
|
-
<HkVersionValue text={hkMergeVersionDisplay(ev, props.engineBuildHash)} />
|
|
571
|
-
</span>
|
|
572
|
-
</div>
|
|
573
|
-
)}
|
|
574
|
-
</>
|
|
575
|
-
)}
|
|
576
|
-
{tier !== "none" && (
|
|
577
|
-
<>
|
|
578
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
579
|
-
<Plug size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
580
|
-
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.protocol", "Protocol")}</span>
|
|
581
|
-
{connecting ? (
|
|
582
|
-
<span style={{ color: "rgb(var(--color-warning))" }}>
|
|
583
|
-
{t("hikari::statusBar.probing", "Probing...")}
|
|
584
|
-
</span>
|
|
585
|
-
) : (
|
|
586
|
-
<span>{t(tierLabelKey, tier)}</span>
|
|
587
|
-
)}
|
|
588
|
-
</div>
|
|
589
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
590
|
-
<Globe size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
591
|
-
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.network", "Network")}</span>
|
|
592
|
-
<span>{regionDisplayName(info.region, locale, t)}{info.asn != null ? ` · AS${info.asn}` : ""}{info.isLocalhost ? " · " + t("hikari::statusBar.local", "Local") : ""}</span>
|
|
593
|
-
</div>
|
|
594
|
-
</>
|
|
595
|
-
)}
|
|
596
|
-
{(props.extraDetails ?? []).map((row) => (
|
|
597
|
-
<div key={row.key} data-extra-detail={row.key} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "6px", maxWidth: "400px" }}>
|
|
598
|
-
<span style={{ opacity: 0.5, flexShrink: 0, display: "inline-flex" }}>{row.icon}</span>
|
|
599
|
-
<span style={{ opacity: 0.5, flexShrink: 0, minWidth: "72px", marginRight: "auto" }}>{row.label}</span>
|
|
600
|
-
<span title={row.value} style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})`, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.value}</span>
|
|
601
|
-
</div>
|
|
602
|
-
))}
|
|
603
|
-
</>
|
|
604
|
-
) : (
|
|
605
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
|
|
606
|
-
<span style={{ opacity: 0.5 }}>{t("hikari::statusBar.fetching", "Fetching connection info...")}</span>
|
|
607
|
-
{hasActionsRow && (
|
|
608
|
-
// An outage that drops connectionInfo to null is exactly
|
|
609
|
-
// when the recovery action is most needed — the cluster
|
|
610
|
-
// rides the fetching line's right edge.
|
|
611
|
-
<span data-status-actions style={{ display: "inline-flex", alignItems: "center", gap: "8px", marginLeft: "auto" }}>
|
|
612
|
-
{actionVnodes}
|
|
505
|
+
{/* The status heading is the popover's title row and it is
|
|
506
|
+
ALWAYS a definite state (2026-10-17 user report: the idle
|
|
507
|
+
auth pill's popover printed "Fetching connection info..."
|
|
508
|
+
forever while its steady gray light meant a settled state —
|
|
509
|
+
a genuinely in-flight light pulses the warning tint, it
|
|
510
|
+
never rests gray). The old info-null fallback is RETIRED;
|
|
511
|
+
the info-dependent rows below simply don't render until
|
|
512
|
+
the probe has facts to show. */}
|
|
513
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px", fontWeight: 600, fontSize: "0.8125rem" }}>
|
|
514
|
+
{qualityIcon(info?.quality || (mode === "connected" ? "good" : "unknown"), tier, info?.isLocalhost ?? false, 14)}
|
|
515
|
+
<span style={{ color: dotColorMap[mode] ?? dotColorMap.disconnected }}>
|
|
516
|
+
{statusText}
|
|
517
|
+
</span>
|
|
518
|
+
{latency !== null && (
|
|
519
|
+
<span style={{ marginLeft: "auto", color: latencyColor(latency), fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})`, fontWeight: 600, fontSize: "0.6875rem" }}>
|
|
520
|
+
{latency} ms
|
|
521
|
+
</span>
|
|
522
|
+
)}
|
|
523
|
+
{hasActionsRow && (
|
|
524
|
+
<span
|
|
525
|
+
data-status-actions
|
|
526
|
+
style={{
|
|
527
|
+
display: "inline-flex",
|
|
528
|
+
alignItems: "center",
|
|
529
|
+
gap: "8px",
|
|
530
|
+
// No latency chip to push against: the cluster
|
|
531
|
+
// still lands on the row's right edge.
|
|
532
|
+
marginLeft: latency !== null ? undefined : "auto",
|
|
533
|
+
}}
|
|
534
|
+
>
|
|
535
|
+
{actionVnodes}
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
{connecting && attempt > 0 && (
|
|
540
|
+
<div style={{ display: "flex", alignItems: "center", gap: "4px", color: "rgb(var(--color-warning))", fontSize: "0.6875rem", marginBottom: "4px" }}>
|
|
541
|
+
<span>
|
|
542
|
+
{t("hikari::statusBar.retrying", "Retrying {retryCount} / {maxRetries}")
|
|
543
|
+
.replace("{retryCount}", String(attempt))
|
|
544
|
+
.replace("{maxRetries}", String(info?.maxRetries && info.maxRetries > 0 ? info.maxRetries : 3))}
|
|
545
|
+
</span>
|
|
546
|
+
{countdown > 0 && (
|
|
547
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: "4px", fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})`, marginLeft: "8px" }}>
|
|
548
|
+
<HkCountdownDigit value={countdown} />
|
|
613
549
|
</span>
|
|
614
550
|
)}
|
|
615
551
|
</div>
|
|
616
552
|
)}
|
|
553
|
+
{mode === "disconnected" && !hasActionsRow && (
|
|
554
|
+
// Fallback for hosts without a slot action — and a REAL
|
|
555
|
+
// control this time: the original hint was retired
|
|
556
|
+
// because its clicks went nowhere (the popover body
|
|
557
|
+
// teleports to <body>), so the text now carries the
|
|
558
|
+
// retry itself, echo guard included.
|
|
559
|
+
<div
|
|
560
|
+
role="button"
|
|
561
|
+
tabindex={0}
|
|
562
|
+
onClick={onRetryButtonClick}
|
|
563
|
+
onKeydown={(e: KeyboardEvent) => {
|
|
564
|
+
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onRetryButtonClick(); }
|
|
565
|
+
}}
|
|
566
|
+
style={{ fontStyle: "italic", fontSize: "0.6875rem", marginBottom: "4px", opacity: 0.7, cursor: "pointer" }}
|
|
567
|
+
>
|
|
568
|
+
{t("hikari::statusBar.clickReconnect", "Click to retry")}
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
{/* Version rows: ALWAYS in the popover (2026-10-17 user
|
|
572
|
+
direction — versions are known facts, show them up front
|
|
573
|
+
independent of the probe state; the old gate hid them
|
|
574
|
+
behind `compact` inside the info branch, which left the
|
|
575
|
+
idle auth popover versionless). The inline tag keeps its
|
|
576
|
+
own copy; this card is the details surface. */}
|
|
577
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
578
|
+
<Monitor size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
579
|
+
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.panel", "Panel")}</span>
|
|
580
|
+
<span
|
|
581
|
+
class="s-status-bar-popover-value"
|
|
582
|
+
style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})` }}
|
|
583
|
+
>
|
|
584
|
+
<HkVersionValue text={pv} />
|
|
585
|
+
</span>
|
|
586
|
+
</div>
|
|
587
|
+
{ev && (
|
|
588
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
589
|
+
<Cog size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
590
|
+
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.engine", "Engine")}</span>
|
|
591
|
+
<span
|
|
592
|
+
class="s-status-bar-popover-value"
|
|
593
|
+
style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})` }}
|
|
594
|
+
>
|
|
595
|
+
<HkVersionValue text={hkMergeVersionDisplay(ev, props.engineBuildHash)} />
|
|
596
|
+
</span>
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
{info && tier !== "none" && tier !== "unknown" && (
|
|
600
|
+
// "unknown" is the inert shell's sentinel tier — no probe
|
|
601
|
+
// is wired, so there are no transport facts to show and a
|
|
602
|
+
// bare "Protocol unknown" row would leak (R3 catch).
|
|
603
|
+
<>
|
|
604
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
605
|
+
<Plug size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
606
|
+
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.protocol", "Protocol")}</span>
|
|
607
|
+
{connecting ? (
|
|
608
|
+
<span style={{ color: "rgb(var(--color-warning))" }}>
|
|
609
|
+
{t("hikari::statusBar.probing", "Probing...")}
|
|
610
|
+
</span>
|
|
611
|
+
) : (
|
|
612
|
+
<span>{t(tierLabelKey, tier)}</span>
|
|
613
|
+
)}
|
|
614
|
+
</div>
|
|
615
|
+
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
616
|
+
<Globe size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
617
|
+
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.network", "Network")}</span>
|
|
618
|
+
<span>{regionDisplayName(info.region, locale, t)}{info.asn != null ? ` · AS${info.asn}` : ""}{info.isLocalhost ? " · " + t("hikari::statusBar.local", "Local") : ""}</span>
|
|
619
|
+
</div>
|
|
620
|
+
</>
|
|
621
|
+
)}
|
|
622
|
+
{info && (props.extraDetails ?? []).map((row) => (
|
|
623
|
+
<div key={row.key} data-extra-detail={row.key} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "6px", maxWidth: "400px" }}>
|
|
624
|
+
<span style={{ opacity: 0.5, flexShrink: 0, display: "inline-flex" }}>{row.icon}</span>
|
|
625
|
+
<span style={{ opacity: 0.5, flexShrink: 0, minWidth: "72px", marginRight: "auto" }}>{row.label}</span>
|
|
626
|
+
<span title={row.value} style={{ fontFamily: `var(--font-mono, ${HIKARI_FONT_MONO})`, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{row.value}</span>
|
|
627
|
+
</div>
|
|
628
|
+
))}
|
|
617
629
|
</div>
|
|
618
630
|
</HPopover>
|
|
619
631
|
</>
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "جارٍ الاتصال...",
|
|
156
156
|
"hikari::statusBar.disconnected": "غير متصل",
|
|
157
157
|
"hikari::statusBar.engine": "المحرك",
|
|
158
|
-
"hikari::statusBar.fetching": "جارٍ جلب معلومات الاتصال...",
|
|
159
158
|
"hikari::statusBar.latency": "زمن الاستجابة",
|
|
160
159
|
"hikari::statusBar.local": "محلي",
|
|
161
160
|
"hikari::statusBar.network": "الشبكة",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "فشل التحميل، انقر لإعادة المحاولة",
|
|
284
283
|
"hikari::search.trigger": "بحث",
|
|
285
284
|
"hikari::search.clear": "مسح البحث",
|
|
286
|
-
"hikari::search.apply": "تطبيق"
|
|
285
|
+
"hikari::search.apply": "تطبيق",
|
|
286
|
+
"hikari::statusBar.idle": "جاهز"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Verbinde...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Getrennt",
|
|
157
157
|
"hikari::statusBar.engine": "Engine",
|
|
158
|
-
"hikari::statusBar.fetching": "Verbindungsinformationen werden geladen...",
|
|
159
158
|
"hikari::statusBar.latency": "Latenz",
|
|
160
159
|
"hikari::statusBar.local": "Lokal",
|
|
161
160
|
"hikari::statusBar.network": "Netzwerk",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Laden fehlgeschlagen, zum Wiederholen klicken",
|
|
284
283
|
"hikari::search.trigger": "Suchen",
|
|
285
284
|
"hikari::search.clear": "Suche löschen",
|
|
286
|
-
"hikari::search.apply": "Anwenden"
|
|
285
|
+
"hikari::search.apply": "Anwenden",
|
|
286
|
+
"hikari::statusBar.idle": "Bereit"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Connecting...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Disconnected",
|
|
157
157
|
"hikari::statusBar.engine": "Engine",
|
|
158
|
-
"hikari::statusBar.fetching": "Fetching connection info...",
|
|
159
158
|
"hikari::statusBar.latency": "Latency",
|
|
160
159
|
"hikari::statusBar.local": "Local",
|
|
161
160
|
"hikari::statusBar.network": "Network",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Load failed, click to retry",
|
|
284
283
|
"hikari::search.trigger": "Search",
|
|
285
284
|
"hikari::search.clear": "Clear search",
|
|
286
|
-
"hikari::search.apply": "Apply"
|
|
285
|
+
"hikari::search.apply": "Apply",
|
|
286
|
+
"hikari::statusBar.idle": "Ready"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Conectando...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Desconectado",
|
|
157
157
|
"hikari::statusBar.engine": "Motor",
|
|
158
|
-
"hikari::statusBar.fetching": "Obteniendo información de conexión...",
|
|
159
158
|
"hikari::statusBar.latency": "Latencia",
|
|
160
159
|
"hikari::statusBar.local": "Local",
|
|
161
160
|
"hikari::statusBar.network": "Red",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Error al cargar, haz clic para reintentar",
|
|
284
283
|
"hikari::search.trigger": "Buscar",
|
|
285
284
|
"hikari::search.clear": "Borrar búsqueda",
|
|
286
|
-
"hikari::search.apply": "Aplicar"
|
|
285
|
+
"hikari::search.apply": "Aplicar",
|
|
286
|
+
"hikari::statusBar.idle": "Listo"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Connexion...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Déconnecté",
|
|
157
157
|
"hikari::statusBar.engine": "Moteur",
|
|
158
|
-
"hikari::statusBar.fetching": "Récupération des informations de connexion...",
|
|
159
158
|
"hikari::statusBar.latency": "Latence",
|
|
160
159
|
"hikari::statusBar.local": "Local",
|
|
161
160
|
"hikari::statusBar.network": "Réseau",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Échec du chargement, cliquez pour réessayer",
|
|
284
283
|
"hikari::search.trigger": "Rechercher",
|
|
285
284
|
"hikari::search.clear": "Effacer la recherche",
|
|
286
|
-
"hikari::search.apply": "Appliquer"
|
|
285
|
+
"hikari::search.apply": "Appliquer",
|
|
286
|
+
"hikari::statusBar.idle": "Prêt"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "接続中...",
|
|
156
156
|
"hikari::statusBar.disconnected": "切断",
|
|
157
157
|
"hikari::statusBar.engine": "エンジン",
|
|
158
|
-
"hikari::statusBar.fetching": "接続情報を取得中...",
|
|
159
158
|
"hikari::statusBar.latency": "レイテンシ",
|
|
160
159
|
"hikari::statusBar.local": "ローカル",
|
|
161
160
|
"hikari::statusBar.network": "ネットワーク地域",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "読み込みに失敗しました。クリックで再試行",
|
|
284
283
|
"hikari::search.trigger": "検索",
|
|
285
284
|
"hikari::search.clear": "検索をクリア",
|
|
286
|
-
"hikari::search.apply": "適用"
|
|
285
|
+
"hikari::search.apply": "適用",
|
|
286
|
+
"hikari::statusBar.idle": "準備完了"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "연결 중...",
|
|
156
156
|
"hikari::statusBar.disconnected": "연결 끊김",
|
|
157
157
|
"hikari::statusBar.engine": "엔진",
|
|
158
|
-
"hikari::statusBar.fetching": "연결 정보를 가져오는 중...",
|
|
159
158
|
"hikari::statusBar.latency": "지연 시간",
|
|
160
159
|
"hikari::statusBar.local": "로컬",
|
|
161
160
|
"hikari::statusBar.network": "네트워크 지역",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "불러오기 실패, 클릭하여 재시도",
|
|
284
283
|
"hikari::search.trigger": "검색",
|
|
285
284
|
"hikari::search.clear": "검색 지우기",
|
|
286
|
-
"hikari::search.apply": "적용"
|
|
285
|
+
"hikari::search.apply": "적용",
|
|
286
|
+
"hikari::statusBar.idle": "준비됨"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Conectando...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Desconectado",
|
|
157
157
|
"hikari::statusBar.engine": "Motor",
|
|
158
|
-
"hikari::statusBar.fetching": "Buscando informações de conexão...",
|
|
159
158
|
"hikari::statusBar.latency": "Latência",
|
|
160
159
|
"hikari::statusBar.local": "Local",
|
|
161
160
|
"hikari::statusBar.network": "Rede",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Falha ao carregar, clique para tentar novamente",
|
|
284
283
|
"hikari::search.trigger": "Pesquisar",
|
|
285
284
|
"hikari::search.clear": "Limpar pesquisa",
|
|
286
|
-
"hikari::search.apply": "Aplicar"
|
|
285
|
+
"hikari::search.apply": "Aplicar",
|
|
286
|
+
"hikari::statusBar.idle": "Pronto"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "Подключение...",
|
|
156
156
|
"hikari::statusBar.disconnected": "Отключено",
|
|
157
157
|
"hikari::statusBar.engine": "Движок",
|
|
158
|
-
"hikari::statusBar.fetching": "Получение сведений о подключении...",
|
|
159
158
|
"hikari::statusBar.latency": "Задержка",
|
|
160
159
|
"hikari::statusBar.local": "Локально",
|
|
161
160
|
"hikari::statusBar.network": "Сеть",
|
|
@@ -283,5 +282,6 @@
|
|
|
283
282
|
"hikari::diff.loadFailed": "Не удалось загрузить, нажмите для повтора",
|
|
284
283
|
"hikari::search.trigger": "Поиск",
|
|
285
284
|
"hikari::search.clear": "Очистить поиск",
|
|
286
|
-
"hikari::search.apply": "Применить"
|
|
285
|
+
"hikari::search.apply": "Применить",
|
|
286
|
+
"hikari::statusBar.idle": "Готово"
|
|
287
287
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "连接中...",
|
|
156
156
|
"hikari::statusBar.disconnected": "断开",
|
|
157
157
|
"hikari::statusBar.engine": "引擎",
|
|
158
|
-
"hikari::statusBar.fetching": "获取连接信息中...",
|
|
159
158
|
"hikari::statusBar.latency": "延迟",
|
|
160
159
|
"hikari::statusBar.local": "本地",
|
|
161
160
|
"hikari::statusBar.network": "网络区域",
|
|
@@ -287,5 +286,6 @@
|
|
|
287
286
|
"hikari::diff.loadFailed": "加载失败,点击重试",
|
|
288
287
|
"hikari::search.trigger": "搜索",
|
|
289
288
|
"hikari::search.clear": "清除搜索",
|
|
290
|
-
"hikari::search.apply": "应用"
|
|
289
|
+
"hikari::search.apply": "应用",
|
|
290
|
+
"hikari::statusBar.idle": "就绪"
|
|
291
291
|
}
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"hikari::statusBar.connecting": "連線中...",
|
|
156
156
|
"hikari::statusBar.disconnected": "斷開",
|
|
157
157
|
"hikari::statusBar.engine": "引擎",
|
|
158
|
-
"hikari::statusBar.fetching": "取得連線資訊中...",
|
|
159
158
|
"hikari::statusBar.latency": "延遲",
|
|
160
159
|
"hikari::statusBar.local": "本機",
|
|
161
160
|
"hikari::statusBar.network": "網路區域",
|
|
@@ -287,5 +286,6 @@
|
|
|
287
286
|
"hikari::diff.loadFailed": "載入失敗,點擊重試",
|
|
288
287
|
"hikari::search.trigger": "搜尋",
|
|
289
288
|
"hikari::search.clear": "清除搜尋",
|
|
290
|
-
"hikari::search.apply": "套用"
|
|
289
|
+
"hikari::search.apply": "套用",
|
|
290
|
+
"hikari::statusBar.idle": "就緒"
|
|
291
291
|
}
|
|
@@ -267,9 +267,33 @@
|
|
|
267
267
|
opacity: 0.5;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/* In-flight (connecting/reconnecting): the warning tint BREATHES —
|
|
271
|
+
2026-10-17 user direction, a fixed background must mean a SETTLED
|
|
272
|
+
state (that is the gray idle's job now), so a genuinely in-flight
|
|
273
|
+
light visibly pulses between a faint and a stronger tint. Pure-CSS
|
|
274
|
+
compositor motion; reduced-motion users keep the steady resting
|
|
275
|
+
tint (the class's original value). */
|
|
270
276
|
.s-status-bar-tag-reconnecting {
|
|
271
277
|
background: rgb(var(--color-warning) / 14%);
|
|
272
278
|
transition: background var(--duration-short), width var(--duration-short) var(--ease-standard);
|
|
279
|
+
animation: s-status-bar-reconnecting-breathe 1.8s var(--ease-standard) infinite;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@keyframes s-status-bar-reconnecting-breathe {
|
|
283
|
+
0%,
|
|
284
|
+
100% {
|
|
285
|
+
background: rgb(var(--color-warning) / 8%);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
50% {
|
|
289
|
+
background: rgb(var(--color-warning) / 22%);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
@media (prefers-reduced-motion: reduce) {
|
|
294
|
+
.s-status-bar-tag-reconnecting {
|
|
295
|
+
animation: none;
|
|
296
|
+
}
|
|
273
297
|
}
|
|
274
298
|
|
|
275
299
|
/* One soft green blink when the light recovers: the pill background
|