@celestia-island/hikari 0.55.105 → 0.55.107
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 +1 -1
- package/src/components/HkConnectionStatus.tsx +6 -1
- package/src/components/HkErrorLanding.inline.contract.test.ts +89 -12
- package/src/components/HkErrorLanding.scss +32 -11
- package/src/components/HkErrorLanding.tsx +4 -2
- package/src/components/HkStatusBar.test.tsx +141 -0
- package/src/components/HkStatusBar.tsx +49 -2
- package/src/i18n/locales/ar/components.json +1 -0
- package/src/i18n/locales/de/components.json +1 -0
- package/src/i18n/locales/en/components.json +1 -0
- package/src/i18n/locales/es/components.json +1 -0
- package/src/i18n/locales/fr/components.json +1 -0
- package/src/i18n/locales/ja/components.json +1 -0
- package/src/i18n/locales/ko/components.json +1 -0
- package/src/i18n/locales/pt/components.json +1 -0
- package/src/i18n/locales/ru/components.json +1 -0
- package/src/i18n/locales/zh-Hans/components.json +1 -0
- package/src/i18n/locales/zh-Hant/components.json +1 -0
package/package.json
CHANGED
|
@@ -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
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Source contract: the inline landing
|
|
3
|
-
* `.is-inline` variant must
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
2
|
+
* Source contract: the inline landing CENTERS inside its host region. The
|
|
3
|
+
* `.is-inline` variant must fill the host container's height (so the base
|
|
4
|
+
* centering seats the card mid-container — the inline twin of the page
|
|
5
|
+
* variant centering in the viewport), carry no block-axis padding (the
|
|
6
|
+
* #695 lesson: a hardcoded top inset read as "floating too low" under a
|
|
7
|
+
* sibling header), and center the card via overflow-safe auto margins
|
|
8
|
+
* (collapse to top-anchored when the card is taller than the container,
|
|
9
|
+
* instead of a both-edge clip nothing can scroll to). The horizontal
|
|
10
|
+
* inset stays so a narrow pane never seats the card edge-to-edge. A
|
|
11
|
+
* class-name test cannot see this; the compiled contract lives here.
|
|
11
12
|
*/
|
|
12
13
|
import { describe, expect, it } from "vitest";
|
|
13
14
|
import { readFileSync } from "node:fs";
|
|
@@ -16,7 +17,32 @@ import { fileURLToPath } from "node:url";
|
|
|
16
17
|
|
|
17
18
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
|
|
19
|
-
/**
|
|
20
|
+
/** Strip SCSS comments — line and block form — so a contract literal can
|
|
21
|
+
* only ever be satisfied by a REAL declaration, never by prose: a
|
|
22
|
+
* commented-out `margin: auto` (line-comment mutation found in R1,
|
|
23
|
+
* block-comment in R2) would otherwise false-green `toContain`. A `//`
|
|
24
|
+
* preceded by `:` is a URL scheme (`https://`) and survives. Block
|
|
25
|
+
* comments go first so a `//` hidden inside them can't leak through. */
|
|
26
|
+
function stripComments(css: string): string {
|
|
27
|
+
return stripLineComments(css.replace(/\/\*[\s\S]*?\*\//g, ""));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Strip SCSS `//` line comments; `://` URL schemes survive. */
|
|
31
|
+
function stripLineComments(css: string): string {
|
|
32
|
+
return css
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => {
|
|
35
|
+
let i = line.indexOf("//");
|
|
36
|
+
while (i !== -1 && line[i - 1] === ":") {
|
|
37
|
+
i = line.indexOf("//", i + 1);
|
|
38
|
+
}
|
|
39
|
+
return i === -1 ? line : line.slice(0, i);
|
|
40
|
+
})
|
|
41
|
+
.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Extract one rule's body with nesting-aware brace matching, prose
|
|
45
|
+
* stripped. */
|
|
20
46
|
function ruleBody(css: string, selector: string): string {
|
|
21
47
|
const start = css.indexOf(`${selector} {`);
|
|
22
48
|
expect(start, `rule ${selector} present`).toBeGreaterThanOrEqual(0);
|
|
@@ -26,23 +52,57 @@ function ruleBody(css: string, selector: string): string {
|
|
|
26
52
|
if (css[i] === "{") depth++;
|
|
27
53
|
else if (css[i] === "}") {
|
|
28
54
|
depth--;
|
|
29
|
-
if (depth === 0) return css.slice(open + 1, i);
|
|
55
|
+
if (depth === 0) return stripComments(css.slice(open + 1, i));
|
|
30
56
|
}
|
|
31
57
|
}
|
|
32
58
|
throw new Error(`unbalanced braces in ${selector}`);
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
describe("contract-test comment stripping", () => {
|
|
62
|
+
it("strips comments (positive control — the guard must be able to bite)", () => {
|
|
63
|
+
// Without this control a broken stripper would silently turn every
|
|
64
|
+
// assertion above into prose-matching; 0-strip must be LOUD.
|
|
65
|
+
const dirty = "// min-height: 100%\n min-height: 100%;";
|
|
66
|
+
expect(stripComments(dirty)).not.toContain("//");
|
|
67
|
+
expect(stripComments(dirty).trim()).toBe("min-height: 100%;");
|
|
68
|
+
// Block comments are prose too (the R2 mutation shape: a commented-out
|
|
69
|
+
// declaration must never satisfy the contract).
|
|
70
|
+
const blocked = "margin: initial; /* margin: auto */";
|
|
71
|
+
expect(stripComments(blocked)).toContain("margin: initial;");
|
|
72
|
+
expect(stripComments(blocked)).not.toContain("margin: auto");
|
|
73
|
+
// URL schemes survive intact.
|
|
74
|
+
expect(stripComments('icon: url("https://cdn.example/x.svg");')).toContain("://");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
35
78
|
describe("HkErrorLanding inline contract", () => {
|
|
36
79
|
const css = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
|
|
37
80
|
const inline = ruleBody(css, ".hk-error-landing.is-inline");
|
|
38
81
|
|
|
82
|
+
it("fills the host region's height so the shared centering can seat the card", () => {
|
|
83
|
+
// `min-height: 0` collapsed the root to its content height, which
|
|
84
|
+
// silently disabled the base align/justify centering and glued the
|
|
85
|
+
// card to the container's top edge.
|
|
86
|
+
expect(inline).toContain("min-height: 100%");
|
|
87
|
+
expect(inline).not.toContain("min-height: 0");
|
|
88
|
+
});
|
|
89
|
+
|
|
39
90
|
it("drops the block-axis padding entirely and keeps only the horizontal inset", () => {
|
|
40
|
-
expect(inline).toContain("min-height: 0");
|
|
41
91
|
expect(inline).toContain("padding: 0 var(--space-12, 0.75rem)");
|
|
42
92
|
// The takeover padding must not survive in any form.
|
|
43
93
|
expect(inline).not.toContain("1.25rem");
|
|
44
94
|
});
|
|
45
95
|
|
|
96
|
+
it("centers the card through overflow-safe auto margins", () => {
|
|
97
|
+
const card = ruleBody(inline, ".hk-error-landing__card");
|
|
98
|
+
// Auto margins absorb positive free space (centering) and collapse to
|
|
99
|
+
// 0 on overflow (no both-edge clip).
|
|
100
|
+
expect(card).toContain("margin: auto");
|
|
101
|
+
// The overflow anchor: with the margins collapsed the card must pin
|
|
102
|
+
// to the container's TOP edge — reachable — never float mid-clip.
|
|
103
|
+
expect(inline).toContain("align-items: flex-start");
|
|
104
|
+
});
|
|
105
|
+
|
|
46
106
|
it("slims the card's top padding below the page-grade value", () => {
|
|
47
107
|
const card = ruleBody(inline, ".hk-error-landing__card");
|
|
48
108
|
// The shorthand's block-start value IS the slim step, not the
|
|
@@ -63,3 +123,20 @@ describe("HkErrorLanding multiline headline contract", () => {
|
|
|
63
123
|
expect(title).toContain("overflow-wrap: anywhere");
|
|
64
124
|
});
|
|
65
125
|
});
|
|
126
|
+
|
|
127
|
+
describe("HkErrorLanding page-variant seating contract", () => {
|
|
128
|
+
const css = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
|
|
129
|
+
const base = ruleBody(css, ".hk-error-landing");
|
|
130
|
+
|
|
131
|
+
it("fills the viewport and centers the card in it", () => {
|
|
132
|
+
// The page variant's whole seating model: a full-viewport backdrop
|
|
133
|
+
// with the card centered in it (standalone error pages, the chest
|
|
134
|
+
// overlay, the reporting takeover all ride this rule). R2 flipped
|
|
135
|
+
// align-items to flex-end and every landing test stayed green — this
|
|
136
|
+
// pin closes that hole.
|
|
137
|
+
expect(base).toContain("min-height: 100vh");
|
|
138
|
+
expect(base).toContain("min-height: 100dvh");
|
|
139
|
+
expect(base).toContain("align-items: center");
|
|
140
|
+
expect(base).toContain("justify-content: center");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -276,21 +276,42 @@
|
|
|
276
276
|
// in-place instead of covering the whole screen, or when a host view
|
|
277
277
|
// renders it as its failed content region right under a sibling header.
|
|
278
278
|
.hk-error-landing.is-inline {
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
// the
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
279
|
+
// Fill the HOST REGION's height so the base centering (the align/justify
|
|
280
|
+
// center on .hk-error-landing — this element matches that selector too,
|
|
281
|
+
// which is how the centering reaches the inline variant) seats the card
|
|
282
|
+
// in the middle
|
|
283
|
+
// of whatever container it landed in — the inline twin of the page
|
|
284
|
+
// variant centering in the viewport. The old zero min-height collapsed
|
|
285
|
+
// the root to its content height, which silently disabled that
|
|
286
|
+
// centering and glued the card to the container's top edge; the #695
|
|
287
|
+
// padding removal traded "a padding-frame too low" for "flush against
|
|
288
|
+
// the header". Both were symptoms of spacing-by-inset; centering in the
|
|
289
|
+
// actual container fixes both hosts at once. A host whose height is
|
|
290
|
+
// auto (a growing content column, not a framed pane) resolves the
|
|
291
|
+
// percentage to nothing and keeps a content-sized, top-seated landing
|
|
292
|
+
// — centering applies exactly as far as the container can express it.
|
|
293
|
+
min-height: 100%;
|
|
294
|
+
// The horizontal inset stays so a narrow pane never seats the card
|
|
295
|
+
// edge-to-edge. No block-axis padding: the gap above/below the card is
|
|
296
|
+
// the centering's job now — a hardcoded top inset is exactly what read
|
|
297
|
+
// as "floating too low" under a sibling header in #695.
|
|
286
298
|
padding: 0 var(--space-12, 0.75rem);
|
|
299
|
+
// Explicit overflow anchor: with the card's auto margins below
|
|
300
|
+
// collapsed there is no free space left to center in, and the alignment
|
|
301
|
+
// that then applies must be the START edge — reachable top, clip only
|
|
302
|
+
// at the unreachable bottom. Spelled out (and pinned by the contract
|
|
303
|
+
// test) rather than left emergent from the inherited center; with free
|
|
304
|
+
// space the margins absorb it before alignment ever applies, so this
|
|
305
|
+
// line only ever shows on overflow.
|
|
306
|
+
align-items: flex-start;
|
|
287
307
|
background: transparent;
|
|
288
308
|
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
309
|
+
// Safe centering: auto margins center the card on both axes while free
|
|
310
|
+
// space exists and collapse to 0 when it doesn't (the overflow case
|
|
311
|
+
// anchored by align-items above). Specificity (0,3,0) also keeps the
|
|
312
|
+
// slim padding below outranking the ≤480px media rule (0,1,0).
|
|
293
313
|
.hk-error-landing__card {
|
|
314
|
+
margin: auto;
|
|
294
315
|
padding: var(--space-16, 1rem) var(--space-28, 1.75rem) var(--space-24, 1.5rem);
|
|
295
316
|
}
|
|
296
317
|
}
|
|
@@ -25,8 +25,10 @@ export type HErrorTone = "error" | "warning" | "info";
|
|
|
25
25
|
* Layout variant of the landing.
|
|
26
26
|
* - `page` (default): owns a full-viewport backdrop — for overlays and
|
|
27
27
|
* standalone error pages.
|
|
28
|
-
* - `inline`: drops the backdrop and
|
|
29
|
-
*
|
|
28
|
+
* - `inline`: drops the backdrop and fills its host region's height
|
|
29
|
+
* instead of the viewport, so the same card centers inside whatever
|
|
30
|
+
* pane captured it (HkErrorBoundary) or whatever content region a host
|
|
31
|
+
* view hands it.
|
|
30
32
|
*/
|
|
31
33
|
export type HErrorLandingVariant = "page" | "inline";
|
|
32
34
|
|
|
@@ -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": "斷開",
|