@celestia-island/hikari 0.55.140 → 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 +1 -1
- package/src/components/HkConnectionStatus.tsx +3 -2
- package/src/components/HkPillToggleGroup.scss +70 -3
- package/src/components/HkPillToggleGroup.test.tsx +63 -0
- package/src/components/HkPillToggleGroup.tsx +18 -4
- package/src/components/HkStatusBar.test.tsx +82 -29
- package/src/components/HkStatusBar.tsx +52 -38
- package/src/composables/useOptionStrip.test.ts +45 -0
- package/src/composables/useOptionStrip.ts +209 -0
- package/src/i18n/locales/ar/components.json +0 -1
- package/src/i18n/locales/de/components.json +0 -1
- package/src/i18n/locales/en/components.json +0 -1
- package/src/i18n/locales/es/components.json +0 -1
- package/src/i18n/locales/fr/components.json +0 -1
- package/src/i18n/locales/ja/components.json +0 -1
- package/src/i18n/locales/ko/components.json +0 -1
- package/src/i18n/locales/pt/components.json +0 -1
- package/src/i18n/locales/ru/components.json +0 -1
- package/src/i18n/locales/zh-Hans/components.json +0 -1
- package/src/i18n/locales/zh-Hant/components.json +0 -1
package/package.json
CHANGED
|
@@ -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
|
|
130
|
-
|
|
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 && (
|
|
@@ -1,15 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HkPillToggleGroup chrome — the segmented pill track. Ported from the
|
|
3
|
-
* wowsp 水表查询 filter bar's option group
|
|
3
|
+
* wowsp 水表查询 filter bar's option group (strip chrome and fade
|
|
4
|
+
* masks token-for-token; the pills keep their pointer cursor — the
|
|
5
|
+
* strip root carries the grab affordance): shared
|
|
4
6
|
* chrome around per-option buttons, the active option carrying the
|
|
5
7
|
* indicator tint directly (multi-select cannot use HkTabs — its sliding
|
|
6
|
-
* indicator is radio semantics).
|
|
8
|
+
* indicator is radio semantics). The track is the ONE-LINE PANNABLE
|
|
9
|
+
* STRIP: overflow hides under edge fades (data-h-overflow masks, the
|
|
10
|
+
* HkScrollContainer contract) and pans by wheel / mouse drag / native
|
|
11
|
+
* touch (the machinery lives in useOptionStrip); phones relax back into
|
|
12
|
+
* a wrapping group (media query below — the sheet carries full width).
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
.hk-pill-group {
|
|
10
16
|
display: flex;
|
|
11
|
-
flex-wrap:
|
|
17
|
+
flex-wrap: nowrap;
|
|
18
|
+
max-width: 100%;
|
|
12
19
|
gap: var(--space-4, 4px);
|
|
20
|
+
overflow-x: auto;
|
|
21
|
+
overflow-y: hidden;
|
|
22
|
+
touch-action: pan-x;
|
|
23
|
+
scrollbar-width: none;
|
|
24
|
+
overscroll-behavior-x: contain;
|
|
25
|
+
|
|
26
|
+
&::-webkit-scrollbar {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
--hk-scroll-fade-size: var(--space-24);
|
|
31
|
+
|
|
32
|
+
&[data-h-overflow="end"] {
|
|
33
|
+
-webkit-mask-image: linear-gradient(to left, transparent 0, #000 var(--hk-scroll-fade-size));
|
|
34
|
+
mask-image: linear-gradient(to left, transparent 0, #000 var(--hk-scroll-fade-size));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&[data-h-overflow="start"] {
|
|
38
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--hk-scroll-fade-size));
|
|
39
|
+
mask-image: linear-gradient(to right, transparent 0, #000 var(--hk-scroll-fade-size));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&[data-h-overflow="both"] {
|
|
43
|
+
-webkit-mask-image: linear-gradient(
|
|
44
|
+
to right,
|
|
45
|
+
transparent 0,
|
|
46
|
+
#000 var(--hk-scroll-fade-size),
|
|
47
|
+
#000 calc(100% - var(--hk-scroll-fade-size)),
|
|
48
|
+
transparent 100%
|
|
49
|
+
);
|
|
50
|
+
mask-image: linear-gradient(
|
|
51
|
+
to right,
|
|
52
|
+
transparent 0,
|
|
53
|
+
#000 var(--hk-scroll-fade-size),
|
|
54
|
+
#000 calc(100% - var(--hk-scroll-fade-size)),
|
|
55
|
+
transparent 100%
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* The whole strip drags; clicks still toggle — the 5px pan threshold
|
|
60
|
+
* keeps plain clicks click-shaped. */
|
|
61
|
+
cursor: grab;
|
|
62
|
+
|
|
63
|
+
&[data-panning] {
|
|
64
|
+
cursor: grabbing;
|
|
65
|
+
user-select: none;
|
|
66
|
+
}
|
|
13
67
|
padding: 2px;
|
|
14
68
|
border-radius: var(--radius-sm);
|
|
15
69
|
border: 1px solid rgb(var(--color-border) / 25%);
|
|
@@ -60,3 +114,16 @@
|
|
|
60
114
|
* pressure would collapse them before the label felt it. */
|
|
61
115
|
flex: 0 0 auto;
|
|
62
116
|
}
|
|
117
|
+
|
|
118
|
+
/* Phones: the sheet carries the full width, nothing pans there — the
|
|
119
|
+
* strip relaxes back into the wrapping group (the wowsp filter bar's
|
|
120
|
+
* rule; the fade masks and the grab affordance name interactions the
|
|
121
|
+
* track no longer has). */
|
|
122
|
+
@media (max-width: 767px) {
|
|
123
|
+
.hk-pill-group {
|
|
124
|
+
flex-wrap: wrap;
|
|
125
|
+
overflow: visible;
|
|
126
|
+
touch-action: auto;
|
|
127
|
+
cursor: default;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -111,4 +111,67 @@ describe("HkPillToggleGroup", () => {
|
|
|
111
111
|
expect(pills[2].querySelector(".hk-pill-group-icon")).toBeNull();
|
|
112
112
|
expect(pills[2].textContent?.trim()).toBe("Jk");
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
it("strip wiring: a drag pans and swallows its trailing click — a plain click still toggles", async () => {
|
|
116
|
+
const toggled: string[] = [];
|
|
117
|
+
const c = mount(
|
|
118
|
+
h(HkPillToggleGroup, {
|
|
119
|
+
label: "P",
|
|
120
|
+
options: OPTIONS,
|
|
121
|
+
selected: new Set<string>(),
|
|
122
|
+
onToggle: (v: string) => toggled.push(v),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
const group = c.querySelector(".hk-pill-group")!;
|
|
126
|
+
const pill = c.querySelectorAll<HTMLButtonElement>(".hk-pill-group-pill")[0];
|
|
127
|
+
|
|
128
|
+
// Press on a pill, stray past the 5px threshold, release: the strip
|
|
129
|
+
// flags the pan and the trailing click is swallowed exactly once.
|
|
130
|
+
pill.dispatchEvent(
|
|
131
|
+
new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse", button: 0, clientX: 100, clientY: 50 }),
|
|
132
|
+
);
|
|
133
|
+
window.dispatchEvent(
|
|
134
|
+
new PointerEvent("pointermove", { pointerType: "mouse", clientX: 120, clientY: 52 }),
|
|
135
|
+
);
|
|
136
|
+
await Promise.resolve();
|
|
137
|
+
expect(group.hasAttribute("data-panning")).toBe(true);
|
|
138
|
+
window.dispatchEvent(new PointerEvent("pointerup", { pointerType: "mouse" }));
|
|
139
|
+
await Promise.resolve();
|
|
140
|
+
expect(group.hasAttribute("data-panning")).toBe(false);
|
|
141
|
+
pill.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
142
|
+
await Promise.resolve();
|
|
143
|
+
expect(toggled, "the pan's trailing click must not toggle").toEqual([]);
|
|
144
|
+
|
|
145
|
+
// The NEXT plain click toggles normally — the swallow is one-shot.
|
|
146
|
+
pill.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
147
|
+
await Promise.resolve();
|
|
148
|
+
expect(toggled).toEqual([OPTIONS[0].value]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("strip wiring: a sub-threshold press never pans and its click toggles", async () => {
|
|
152
|
+
const toggled: string[] = [];
|
|
153
|
+
const c = mount(
|
|
154
|
+
h(HkPillToggleGroup, {
|
|
155
|
+
label: "P",
|
|
156
|
+
options: OPTIONS,
|
|
157
|
+
selected: new Set<string>(),
|
|
158
|
+
onToggle: (v: string) => toggled.push(v),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const group = c.querySelector(".hk-pill-group")!;
|
|
162
|
+
const pill = c.querySelectorAll<HTMLButtonElement>(".hk-pill-group-pill")[1];
|
|
163
|
+
|
|
164
|
+
pill.dispatchEvent(
|
|
165
|
+
new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse", button: 0, clientX: 100, clientY: 50 }),
|
|
166
|
+
);
|
|
167
|
+
window.dispatchEvent(
|
|
168
|
+
new PointerEvent("pointermove", { pointerType: "mouse", clientX: 103, clientY: 51 }),
|
|
169
|
+
);
|
|
170
|
+
await Promise.resolve();
|
|
171
|
+
expect(group.hasAttribute("data-panning"), "3px stays a click").toBe(false);
|
|
172
|
+
window.dispatchEvent(new PointerEvent("pointerup", { pointerType: "mouse" }));
|
|
173
|
+
pill.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
expect(toggled).toEqual([OPTIONS[1].value]);
|
|
176
|
+
});
|
|
114
177
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineComponent, h, type PropType, type VNode } from "vue";
|
|
2
2
|
|
|
3
|
+
import { useOptionStrip } from "../composables/useOptionStrip";
|
|
3
4
|
import "./HkPillToggleGroup.scss";
|
|
4
5
|
|
|
5
6
|
/** One toggleable pill of the group. */
|
|
@@ -30,9 +31,13 @@ function resolveIcon(icon: unknown): VNode | null {
|
|
|
30
31
|
* chrome around per-option toggle buttons, the active option carrying
|
|
31
32
|
* the indicator tint directly (multi-select cannot use HkTabs — its
|
|
32
33
|
* sliding indicator is radio semantics). Grammar ported from the wowsp
|
|
33
|
-
* 水表查询 filter bar's option strip (2026-10-11)
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* 水表查询 filter bar's option strip (2026-10-11); the track is the
|
|
35
|
+
* ONE-LINE PANNABLE STRIP (same-day follow-up direction): overflow
|
|
36
|
+
* hides under edge fades and pans by wheel / mouse drag / native touch,
|
|
37
|
+
* exactly the 水表 popups' behavior — a wrapping track always measured
|
|
38
|
+
* ONE long line inside HkPopover's max-content panel and ran off-screen
|
|
39
|
+
* (wowsp #667). Phones relax back into a wrapping group (the sheet
|
|
40
|
+
* carries full width; nothing pans there) via the SCSS media query.
|
|
36
41
|
*
|
|
37
42
|
* Deliberately THIN (the host owns the set): `toggle(value)` reports one
|
|
38
43
|
* pill toggled; the optional leading all-pill reports `all()` and the
|
|
@@ -60,8 +65,17 @@ export default defineComponent({
|
|
|
60
65
|
all: () => true,
|
|
61
66
|
},
|
|
62
67
|
setup(props, { emit }) {
|
|
68
|
+
const { stripEl, panning, onPointerDown } = useOptionStrip();
|
|
69
|
+
|
|
63
70
|
return () => (
|
|
64
|
-
<div
|
|
71
|
+
<div
|
|
72
|
+
ref={stripEl}
|
|
73
|
+
class="hk-pill-group"
|
|
74
|
+
role="group"
|
|
75
|
+
aria-label={props.label}
|
|
76
|
+
data-panning={panning.value || undefined}
|
|
77
|
+
onPointerdown={onPointerDown}
|
|
78
|
+
>
|
|
65
79
|
{props.allLabel != null && (
|
|
66
80
|
<button
|
|
67
81
|
type="button"
|
|
@@ -422,7 +422,7 @@ describe("HkStatusBar", () => {
|
|
|
422
422
|
}
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
-
it("
|
|
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
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// the
|
|
443
|
-
|
|
444
|
-
expect(
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
expect(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
521
|
-
const
|
|
522
|
-
const container =
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
535
|
-
// drops connectionInfo to null is exactly when the
|
|
536
|
-
// most needed.
|
|
537
|
-
const
|
|
538
|
-
expect(
|
|
539
|
-
|
|
540
|
-
expect(
|
|
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
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// The built-in
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
//
|
|
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
|
|
395
|
-
//
|
|
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 =
|
|
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" && !
|
|
513
|
-
|
|
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={{
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
display: "flex",
|
|
575
|
-
|
|
576
|
-
|
|
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>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
WHEEL_LINE_PX,
|
|
5
|
+
overflowSides,
|
|
6
|
+
panEngaged,
|
|
7
|
+
stripWheelDelta,
|
|
8
|
+
} from "./useOptionStrip";
|
|
9
|
+
|
|
10
|
+
describe("stripWheelDelta", () => {
|
|
11
|
+
it("lets a real horizontal wheel delta win over the vertical one", () => {
|
|
12
|
+
expect(stripWheelDelta(0, -120, 40)).toBe(-120);
|
|
13
|
+
});
|
|
14
|
+
it("translates a plain vertical notch onto the horizontal axis", () => {
|
|
15
|
+
expect(stripWheelDelta(0, 0, 80)).toBe(80);
|
|
16
|
+
expect(stripWheelDelta(0, 0, -80)).toBe(-80);
|
|
17
|
+
});
|
|
18
|
+
it("normalizes Firefox line mode to pixels per notch", () => {
|
|
19
|
+
expect(stripWheelDelta(1, 0, 3)).toBe(3 * WHEEL_LINE_PX);
|
|
20
|
+
expect(stripWheelDelta(1, -2, 0)).toBe(-2 * WHEEL_LINE_PX);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("panEngaged", () => {
|
|
25
|
+
it("engages at the threshold on EITHER axis", () => {
|
|
26
|
+
expect(panEngaged(5, 0, 5)).toBe(true);
|
|
27
|
+
expect(panEngaged(0, -5, 5)).toBe(true);
|
|
28
|
+
expect(panEngaged(4, 4, 5)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("overflowSides", () => {
|
|
33
|
+
function fake(scrollWidth: number, clientWidth: number, scrollLeft: number): HTMLElement {
|
|
34
|
+
return { scrollWidth, clientWidth, scrollLeft } as unknown as HTMLElement;
|
|
35
|
+
}
|
|
36
|
+
it("reports none when the strip fits", () => {
|
|
37
|
+
expect(overflowSides(fake(100, 120, 0))).toBe("none");
|
|
38
|
+
});
|
|
39
|
+
it("reports the edges that still hide content (sub-pixel resting counts)", () => {
|
|
40
|
+
expect(overflowSides(fake(200, 100, 0))).toBe("end");
|
|
41
|
+
expect(overflowSides(fake(200, 100, 100))).toBe("start");
|
|
42
|
+
expect(overflowSides(fake(200, 100, 40))).toBe("both");
|
|
43
|
+
expect(overflowSides(fake(200, 100, 0.5))).toBe("end");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOptionStrip — the one-line pannable option track behind
|
|
3
|
+
* HkPillToggleGroup's strip mode. Upstreamed from wowsp's optionStrip
|
|
4
|
+
* (the 水表 filter bar's nation popup and every FilterCategoryChip
|
|
5
|
+
* popup, 2026-10-11 direction: the pill groups must pan with edge
|
|
6
|
+
* fades, not wrap). One module owns:
|
|
7
|
+
*
|
|
8
|
+
* - the wheel→horizontal pan rule (a real horizontal delta wins; a
|
|
9
|
+
* vertical notch is translated onto the strip; Firefox line mode is
|
|
10
|
+
* normalized — mirrors HkScrollContainer's wheel rule);
|
|
11
|
+
* - the mouse drag-pan (pointer capture + the 5px click/pan threshold;
|
|
12
|
+
* the click a pan leaves behind is swallowed exactly once, so panning
|
|
13
|
+
* never toggles an option);
|
|
14
|
+
* - the overflow-side sensing behind the edge fades: the strip mirrors
|
|
15
|
+
* HkScrollContainer's `data-h-overflow` contract (which inline edges
|
|
16
|
+
* still hide content), and the host SCSS turns the value into the
|
|
17
|
+
* matching mask-image gradients.
|
|
18
|
+
*
|
|
19
|
+
* The track element itself stays owned by the host component (its
|
|
20
|
+
* classes and sheet-mode overrides live there); the composable only
|
|
21
|
+
* wires listeners while the element is mounted.
|
|
22
|
+
*/
|
|
23
|
+
import { onBeforeUnmount, ref, watch, type Ref } from "vue";
|
|
24
|
+
|
|
25
|
+
/** Firefox line-height wheel mode (deltaMode 1): one notch reports ~3
|
|
26
|
+
* lines, not pixels — normalized here so a notch pans a wheel-like
|
|
27
|
+
* distance on every engine (40px/line, the classic WebKit line height).
|
|
28
|
+
* Page mode (deltaMode 2, effectively extinct) passes through raw. */
|
|
29
|
+
export const WHEEL_LINE_PX = 40;
|
|
30
|
+
|
|
31
|
+
/** Horizontal pan distance for a wheel gesture over the strip: a real
|
|
32
|
+
* horizontal wheel/trackpad swipe (deltaX — shift+wheel reports as
|
|
33
|
+
* deltaX natively too) wins; a plain vertical wheel notch is translated
|
|
34
|
+
* onto the horizontal axis instead. */
|
|
35
|
+
export function stripWheelDelta(deltaMode: number, deltaX: number, deltaY: number): number {
|
|
36
|
+
const raw = deltaX !== 0 ? deltaX : deltaY;
|
|
37
|
+
return deltaMode === 1 ? raw * WHEEL_LINE_PX : raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Press→pan decision for the strip drag: once the pointer strays
|
|
41
|
+
* ≥ threshold px on EITHER axis the press turns into a pan; below it the
|
|
42
|
+
* gesture stays a click (and clicks must keep toggling options). */
|
|
43
|
+
export function panEngaged(dx: number, dy: number, threshold: number): boolean {
|
|
44
|
+
return Math.abs(dx) >= threshold || Math.abs(dy) >= threshold;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Which inline edges of a strip still hide content — the value mirrored
|
|
48
|
+
* onto the element as `data-h-overflow` (HkScrollContainer's contract;
|
|
49
|
+
* the SCSS maps each value to its edge-fade mask). Sub-pixel scroll
|
|
50
|
+
* offsets count as resting at the edge. RTL flips the visual sides; the
|
|
51
|
+
* family ships LTR only. */
|
|
52
|
+
export type OverflowSides = "none" | "start" | "end" | "both";
|
|
53
|
+
|
|
54
|
+
export function overflowSides(el: HTMLElement): OverflowSides {
|
|
55
|
+
const max = el.scrollWidth - el.clientWidth;
|
|
56
|
+
if (max <= 0) return "none";
|
|
57
|
+
const EDGE_PX = 1;
|
|
58
|
+
const atStart = el.scrollLeft <= EDGE_PX;
|
|
59
|
+
const atEnd = max - el.scrollLeft <= EDGE_PX;
|
|
60
|
+
if (atStart && atEnd) return "none";
|
|
61
|
+
return atStart ? "end" : atEnd ? "start" : "both";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Wire one strip element with the pan + fade machinery. The host binds
|
|
65
|
+
* `stripEl` (template ref), mirrors `panning` into `data-panning`, and
|
|
66
|
+
* attaches `onPointerDown`; the non-passive wheel listener, the
|
|
67
|
+
* capture-phase click swallow and the overflow sensing ride the ref
|
|
68
|
+
* transitions here (a JSX onWheel prop is passive in some paths and
|
|
69
|
+
* could not preventDefault, and JSX has no capture-phase click prop). */
|
|
70
|
+
export function useOptionStrip() {
|
|
71
|
+
const stripEl: Ref<HTMLElement | null> = ref(null);
|
|
72
|
+
const panning = ref(false);
|
|
73
|
+
const PAN_THRESHOLD = 5;
|
|
74
|
+
let pressX = 0;
|
|
75
|
+
let pressY = 0;
|
|
76
|
+
let lastX = 0;
|
|
77
|
+
let armed = false;
|
|
78
|
+
/** Set when a gesture turned into a pan: the trailing click (and only
|
|
79
|
+
* that one) must not toggle an option. */
|
|
80
|
+
let dragged = false;
|
|
81
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
82
|
+
let mutationObserver: MutationObserver | null = null;
|
|
83
|
+
|
|
84
|
+
/** Re-read the overflow sides and mirror them onto the element. Fires
|
|
85
|
+
* on scroll, on element resizes and on content mutations (options
|
|
86
|
+
* re-rendering can change the track's scroll width). */
|
|
87
|
+
function sense() {
|
|
88
|
+
const el = stripEl.value;
|
|
89
|
+
if (!el) return;
|
|
90
|
+
const sides = overflowSides(el);
|
|
91
|
+
if (sides === "none") delete el.dataset.hOverflow;
|
|
92
|
+
else el.dataset.hOverflow = sides;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onWheel(e: WheelEvent) {
|
|
96
|
+
const el = stripEl.value;
|
|
97
|
+
if (!el || el.scrollWidth <= el.clientWidth) return;
|
|
98
|
+
const delta = stripWheelDelta(e.deltaMode, e.deltaX, e.deltaY);
|
|
99
|
+
if (delta === 0) return;
|
|
100
|
+
const prev = el.scrollLeft;
|
|
101
|
+
el.scrollLeft += delta;
|
|
102
|
+
// A saturated edge lets the wheel keep scrolling the page behind.
|
|
103
|
+
if (el.scrollLeft !== prev) e.preventDefault();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function onPointerDown(e: PointerEvent) {
|
|
107
|
+
if (e.pointerType !== "mouse" || e.button !== 0) return;
|
|
108
|
+
// A pan released off-strip leaves no click behind; clear the stale
|
|
109
|
+
// swallow flag so this press's own click always lands.
|
|
110
|
+
dragged = false;
|
|
111
|
+
pressX = lastX = e.clientX;
|
|
112
|
+
pressY = e.clientY;
|
|
113
|
+
armed = false;
|
|
114
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
115
|
+
window.addEventListener("pointerup", onPointerUp, { once: true });
|
|
116
|
+
window.addEventListener("pointercancel", onPointerCancel, { once: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function onPointerMove(e: PointerEvent) {
|
|
120
|
+
if (!armed) {
|
|
121
|
+
if (!panEngaged(e.clientX - pressX, e.clientY - pressY, PAN_THRESHOLD)) return;
|
|
122
|
+
// Engage: capture the pointer so the pan keeps tracking off-panel,
|
|
123
|
+
// flag the strip (grabbing cursor + selection off) and arm the
|
|
124
|
+
// one-shot click swallow — with capture held, the trailing click
|
|
125
|
+
// retargets to the strip anyway, never onto an option.
|
|
126
|
+
armed = true;
|
|
127
|
+
panning.value = true;
|
|
128
|
+
dragged = true;
|
|
129
|
+
stripEl.value?.setPointerCapture(e.pointerId);
|
|
130
|
+
}
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
const el = stripEl.value;
|
|
133
|
+
if (!el) return;
|
|
134
|
+
el.scrollLeft -= e.clientX - lastX;
|
|
135
|
+
lastX = e.clientX;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function teardownPress() {
|
|
139
|
+
armed = false;
|
|
140
|
+
panning.value = false;
|
|
141
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onPointerUp() {
|
|
145
|
+
teardownPress();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function onPointerCancel() {
|
|
149
|
+
// A cancelled gesture never leaves a click — no swallow to hold.
|
|
150
|
+
dragged = false;
|
|
151
|
+
teardownPress();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The click a completed pan leaves behind must never toggle an
|
|
155
|
+
* option: swallowed exactly once, capture phase (ahead of every
|
|
156
|
+
* option handler). Plain clicks pass untouched. */
|
|
157
|
+
function onClickCapture(e: MouseEvent) {
|
|
158
|
+
if (!dragged) return;
|
|
159
|
+
dragged = false;
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A host popup that closed before the pan's trailing click (window
|
|
164
|
+
* blur mid-drag — pointerup lost) must not carry the swallow flag
|
|
165
|
+
* into the reopened panel. */
|
|
166
|
+
function resetDragged() {
|
|
167
|
+
dragged = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
watch(stripEl, (el, prev) => {
|
|
171
|
+
prev?.removeEventListener("wheel", onWheel);
|
|
172
|
+
prev?.removeEventListener("click", onClickCapture, true);
|
|
173
|
+
prev?.removeEventListener("scroll", sense);
|
|
174
|
+
resizeObserver?.disconnect();
|
|
175
|
+
resizeObserver = null;
|
|
176
|
+
mutationObserver?.disconnect();
|
|
177
|
+
mutationObserver = null;
|
|
178
|
+
if (el) {
|
|
179
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
180
|
+
el.addEventListener("click", onClickCapture, true);
|
|
181
|
+
el.addEventListener("scroll", sense, { passive: true });
|
|
182
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
183
|
+
resizeObserver = new ResizeObserver(sense);
|
|
184
|
+
resizeObserver.observe(el);
|
|
185
|
+
}
|
|
186
|
+
if (typeof MutationObserver !== "undefined") {
|
|
187
|
+
mutationObserver = new MutationObserver(sense);
|
|
188
|
+
mutationObserver.observe(el, { childList: true, subtree: true });
|
|
189
|
+
}
|
|
190
|
+
sense();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
onBeforeUnmount(() => {
|
|
194
|
+
teardownPress();
|
|
195
|
+
// Full listener teardown: a component unmounting mid-pan must not
|
|
196
|
+
// leave listeners behind on the strip element or the window's
|
|
197
|
+
// once-listeners unheld.
|
|
198
|
+
const el = stripEl.value;
|
|
199
|
+
el?.removeEventListener("wheel", onWheel);
|
|
200
|
+
el?.removeEventListener("click", onClickCapture, true);
|
|
201
|
+
el?.removeEventListener("scroll", sense);
|
|
202
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
203
|
+
window.removeEventListener("pointercancel", onPointerCancel);
|
|
204
|
+
resizeObserver?.disconnect();
|
|
205
|
+
mutationObserver?.disconnect();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { stripEl, panning, onPointerDown, resetDragged };
|
|
209
|
+
}
|
|
@@ -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": "斷開",
|