@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.105",
3
+ "version": "0.55.107",
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",
@@ -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 sits FLUSH under its context. The
3
- * `.is-inline` variant must drop the viewport takeover's block-axis
4
- * padding entirely (the host pane already pads and a sibling header sits
5
- * immediately above — the old 1.25rem top padding floated the card a
6
- * full padding-frame below its context) and slim the card's page-grade
7
- * top padding (2.25rem desktop / 1.75rem mobile) to one space step. The
8
- * horizontal inset stays so a narrow pane never seats the card
9
- * edge-to-edge. A class-name test cannot see this; the compiled contract
10
- * lives here.
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
- /** Extract one rule's body with nesting-aware brace matching. */
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
- min-height: 0;
280
- // In-pane, the host region already pads and a sibling header usually
281
- // sits immediately above: the takeover's block-axis padding floated
282
- // the card a full padding-frame below its context. The vertical
283
- // padding is dropped entirely (the card's own slimmer top padding
284
- // below supplies the breathing room); the horizontal inset stays so a
285
- // narrow pane never seats the card edge-to-edge.
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
- // The card's page-grade top padding (2.25rem desktop / 1.75rem mobile)
290
- // reads as a detached float inside a pane — slim it to one space step.
291
- // Specificity (0,3,0) outranks the ≤480px media rule (0,1,0), so this
292
- // holds on phones too.
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 viewport height so the same card can
29
- * live inside a pane captured by HkErrorBoundary.
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": "斷開",