@celestia-island/hikari 0.55.92 → 0.55.93
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/HkAttachmentModal.tsx +1 -1
- package/src/components/HkCaptchaModal.tsx +1 -1
- package/src/components/HkCaptchaWidget.tsx +1 -1
- package/src/components/HkCookieConsent.test.tsx +1 -1
- package/src/components/HkCookieConsent.tsx +1 -1
- package/src/components/HkErrorBoundary.test.tsx +11 -0
- package/src/components/HkErrorBoundary.tsx +9 -9
- package/src/components/HkErrorLanding.inline.contract.test.ts +12 -0
- package/src/components/HkErrorLanding.scss +5 -0
- package/src/components/HkErrorLanding.test.tsx +70 -2
- package/src/components/HkErrorLanding.tsx +59 -2
- package/src/components/HkLogWindow.tsx +1 -1
- package/src/components/HkTokenUsagePanel.tsx +1 -1
- package/src/components/HkVoiceInputPopup.tsx +1 -1
- package/src/errorReporting/HkErrorReportingOverlay.tsx +16 -2
- package/src/errorReporting/index.test.tsx +3 -0
- package/src/i18n/flatKeyParity.test.ts +28 -13
- package/src/i18n/locales/ar/chat.json +2 -2
- package/src/i18n/locales/ar/components.json +2 -3
- package/src/i18n/locales/ar/components.toml +0 -1
- package/src/i18n/locales/ar/errors.json +3 -1
- package/src/i18n/locales/ar/platform.json +8 -8
- package/src/i18n/locales/de/chat.json +2 -2
- package/src/i18n/locales/de/components.json +2 -3
- package/src/i18n/locales/de/components.toml +0 -1
- package/src/i18n/locales/de/errors.json +3 -1
- package/src/i18n/locales/de/platform.json +8 -8
- package/src/i18n/locales/en/chat.json +2 -2
- package/src/i18n/locales/en/components.json +2 -3
- package/src/i18n/locales/en/components.toml +0 -1
- package/src/i18n/locales/en/errors.json +3 -1
- package/src/i18n/locales/en/platform.json +8 -8
- package/src/i18n/locales/es/chat.json +2 -2
- package/src/i18n/locales/es/components.json +2 -3
- package/src/i18n/locales/es/components.toml +0 -1
- package/src/i18n/locales/es/errors.json +3 -1
- package/src/i18n/locales/es/platform.json +8 -8
- package/src/i18n/locales/fr/chat.json +2 -2
- package/src/i18n/locales/fr/components.json +2 -3
- package/src/i18n/locales/fr/components.toml +0 -1
- package/src/i18n/locales/fr/errors.json +3 -1
- package/src/i18n/locales/fr/platform.json +8 -8
- package/src/i18n/locales/ja/chat.json +2 -2
- package/src/i18n/locales/ja/components.json +2 -3
- package/src/i18n/locales/ja/components.toml +0 -1
- package/src/i18n/locales/ja/errors.json +3 -1
- package/src/i18n/locales/ja/platform.json +8 -8
- package/src/i18n/locales/ko/chat.json +2 -2
- package/src/i18n/locales/ko/components.json +2 -3
- package/src/i18n/locales/ko/components.toml +0 -1
- package/src/i18n/locales/ko/errors.json +3 -1
- package/src/i18n/locales/ko/platform.json +8 -8
- package/src/i18n/locales/pt/chat.json +2 -2
- package/src/i18n/locales/pt/components.json +2 -3
- package/src/i18n/locales/pt/components.toml +0 -1
- package/src/i18n/locales/pt/errors.json +3 -1
- package/src/i18n/locales/pt/platform.json +8 -8
- package/src/i18n/locales/ru/chat.json +2 -2
- package/src/i18n/locales/ru/components.json +2 -3
- package/src/i18n/locales/ru/components.toml +0 -1
- package/src/i18n/locales/ru/errors.json +3 -1
- package/src/i18n/locales/ru/platform.json +8 -8
- package/src/i18n/locales/zh-Hans/chat.json +2 -2
- package/src/i18n/locales/zh-Hans/components.json +2 -3
- package/src/i18n/locales/zh-Hans/components.toml +0 -1
- package/src/i18n/locales/zh-Hans/errors.json +3 -1
- package/src/i18n/locales/zh-Hans/platform.json +8 -8
- package/src/i18n/locales/zh-Hant/chat.json +2 -2
- package/src/i18n/locales/zh-Hant/components.json +2 -3
- package/src/i18n/locales/zh-Hant/components.toml +0 -1
- package/src/i18n/locales/zh-Hant/errors.json +3 -1
- package/src/i18n/locales/zh-Hant/platform.json +8 -8
package/package.json
CHANGED
|
@@ -286,7 +286,7 @@ export const HkAttachmentModal = defineComponent({
|
|
|
286
286
|
<p class="s-attachment-modal-text-empty">
|
|
287
287
|
{srcError.value
|
|
288
288
|
? srcError.value
|
|
289
|
-
: t("hikari::attachment.noPreview", "No preview available
|
|
289
|
+
: t("hikari::attachment.noPreview", "No preview available")}
|
|
290
290
|
</p>
|
|
291
291
|
)}
|
|
292
292
|
{textError.value && (
|
|
@@ -41,7 +41,7 @@ export const HkCaptchaModal = defineComponent({
|
|
|
41
41
|
width={props.width}
|
|
42
42
|
>
|
|
43
43
|
<div class="s-captcha-modal">
|
|
44
|
-
<p class="s-captcha-modal-prompt">{t("hikari::captcha.prompt", "Complete the verification below to continue
|
|
44
|
+
<p class="s-captcha-modal-prompt">{t("hikari::captcha.prompt", "Complete the verification below to continue")}</p>
|
|
45
45
|
{props.modelValue && (
|
|
46
46
|
<HkCaptchaWidget
|
|
47
47
|
siteKey={props.siteKey}
|
|
@@ -166,7 +166,7 @@ export const HkCaptchaWidget = defineComponent({
|
|
|
166
166
|
return (
|
|
167
167
|
<div class="s-captcha-widget s-captcha-widget-placeholder">
|
|
168
168
|
<ShieldAlert size={16} />
|
|
169
|
-
<span>{t("hikari::captcha.placeholder", "Verification is not configured for this deployment (no site key provided)
|
|
169
|
+
<span>{t("hikari::captcha.placeholder", "Verification is not configured for this deployment (no site key provided)")}</span>
|
|
170
170
|
</div>
|
|
171
171
|
);
|
|
172
172
|
}
|
|
@@ -51,7 +51,7 @@ describe("HkCookieConsent", () => {
|
|
|
51
51
|
await nextTick();
|
|
52
52
|
const notice = container.querySelector<HTMLElement>(".s-cookie-consent");
|
|
53
53
|
expect(notice, "notice span renders").toBeTruthy();
|
|
54
|
-
expect(notice!.textContent).toContain("This site uses cookies
|
|
54
|
+
expect(notice!.textContent).toContain("This site uses cookies");
|
|
55
55
|
const ok = container.querySelector<HTMLButtonElement>(".s-cookie-consent-ok");
|
|
56
56
|
expect(ok, "OK button renders").toBeTruthy();
|
|
57
57
|
expect(ok!.textContent).toBe("OK");
|
|
@@ -45,7 +45,7 @@ export const HkCookieConsent = defineComponent({
|
|
|
45
45
|
if (accepted.value) return <Cookie size={12} class="s-cookie-consent-icon" />;
|
|
46
46
|
return (
|
|
47
47
|
<span class="s-cookie-consent">
|
|
48
|
-
{t("hikari::cookie.text", "This site uses cookies
|
|
48
|
+
{t("hikari::cookie.text", "This site uses cookies")}
|
|
49
49
|
<button type="button" class="s-cookie-consent-ok" onClick={accept}>
|
|
50
50
|
{t("hikari::cookie.ok", "OK")}
|
|
51
51
|
</button>
|
|
@@ -91,6 +91,17 @@ describe("HkErrorBoundary", () => {
|
|
|
91
91
|
expect(el.querySelector(".hk-error-landing")).not.toBeNull();
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it("feeds the landing's built-in copy action with the captured error", async () => {
|
|
95
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
96
|
+
const el = mountWith(Boom, { copyErrorLabel: "Copy Error" });
|
|
97
|
+
await nextTick();
|
|
98
|
+
|
|
99
|
+
const buttons = Array.from(el.querySelectorAll<HTMLButtonElement>(".hk-error-landing__actions button"));
|
|
100
|
+
// Built-in copy seats first, the boundary's own retry follows.
|
|
101
|
+
expect(buttons[0]!.textContent).toBe("Copy Error");
|
|
102
|
+
expect(buttons.some((b) => b.textContent === "Retry")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
94
105
|
it("keeps the custom fallback render prop contract", async () => {
|
|
95
106
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
96
107
|
const el = mountWith(Boom, {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RefreshCw } from "lucide-vue-next";
|
|
2
2
|
import { computed, defineComponent, onErrorCaptured, ref, type PropType, type VNode } from "vue";
|
|
3
3
|
|
|
4
|
-
import { useClipboard } from "../runtime/useClipboard";
|
|
5
4
|
import { useI18n } from "../i18n/context";
|
|
6
5
|
import HButton from "./HkButton";
|
|
7
6
|
import { HkErrorLanding } from "./HkErrorLanding";
|
|
@@ -31,8 +30,9 @@ function formatError(err: CapturedError): string {
|
|
|
31
30
|
* The built-in fallback is the same HkErrorLanding card the family's
|
|
32
31
|
* full-page takeovers use (inline variant): tone icon, headline, the error
|
|
33
32
|
* name as the tone-matched HkBadge chip, the message as the description,
|
|
34
|
-
* the raw name/message/stack in the fixed-height JSON tree pane, plus
|
|
35
|
-
* retry
|
|
33
|
+
* the raw name/message/stack in the fixed-height JSON tree pane, plus the
|
|
34
|
+
* retry action and the landing's built-in copy action (fed with the
|
|
35
|
+
* formatted error text).
|
|
36
36
|
*/
|
|
37
37
|
export default defineComponent({
|
|
38
38
|
name: "HkErrorBoundary",
|
|
@@ -44,7 +44,6 @@ export default defineComponent({
|
|
|
44
44
|
retryLabel: { type: String, default: "" },
|
|
45
45
|
},
|
|
46
46
|
setup(props, { slots }) {
|
|
47
|
-
const clipboard = useClipboard();
|
|
48
47
|
const { t } = useI18n();
|
|
49
48
|
const error = ref<CapturedError | null>(null);
|
|
50
49
|
|
|
@@ -87,6 +86,11 @@ export default defineComponent({
|
|
|
87
86
|
title={props.errorTitle || t("hikari::errors.defaultTitle", "Something went wrong")}
|
|
88
87
|
description={err.message}
|
|
89
88
|
code={err.name}
|
|
89
|
+
// The copy action is the landing's built-in now (seated left of
|
|
90
|
+
// the host actions, one design family-wide); this boundary only
|
|
91
|
+
// feeds it the formatted error and its legacy label override.
|
|
92
|
+
copyText={formatError(err)}
|
|
93
|
+
copyLabel={props.copyErrorLabel}
|
|
90
94
|
>
|
|
91
95
|
{{
|
|
92
96
|
default: () => <HkJsonTree value={detailsValue.value} ariaLabel="stack trace" />,
|
|
@@ -95,10 +99,6 @@ export default defineComponent({
|
|
|
95
99
|
<RefreshCw size={12} />
|
|
96
100
|
{props.retryLabel || t("hikari::errorBoundary.retry", "Retry")}
|
|
97
101
|
</HButton>,
|
|
98
|
-
<HButton variant="ghost" size="sm" onClick={() => clipboard.copy(formatError(err))}>
|
|
99
|
-
<Copy size={12} />
|
|
100
|
-
{props.copyErrorLabel || t("hikari::errorBoundary.copyError", "Copy Error")}
|
|
101
|
-
</HButton>,
|
|
102
102
|
],
|
|
103
103
|
}}
|
|
104
104
|
</HkErrorLanding>
|
|
@@ -51,3 +51,15 @@ describe("HkErrorLanding inline contract", () => {
|
|
|
51
51
|
expect(card).not.toContain("2.25rem");
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
describe("HkErrorLanding multiline headline contract", () => {
|
|
56
|
+
const css = readFileSync(join(here, "HkErrorLanding.scss"), "utf-8");
|
|
57
|
+
const title = ruleBody(css, ".hk-error-landing__title");
|
|
58
|
+
|
|
59
|
+
it("renders authored line breaks in the headline as breaks", () => {
|
|
60
|
+
// Catalogs ship per-line copy (a dash clause moved onto its own
|
|
61
|
+
// line); without pre-line the h1 collapses those breaks to spaces.
|
|
62
|
+
expect(title).toContain("white-space: pre-line");
|
|
63
|
+
expect(title).toContain("overflow-wrap: anywhere");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -173,6 +173,11 @@
|
|
|
173
173
|
letter-spacing: -0.01em;
|
|
174
174
|
line-height: 1.35;
|
|
175
175
|
color: rgb(var(--hel-text));
|
|
176
|
+
// Headlines may carry their own line breaks (hosts ship per-line copy,
|
|
177
|
+
// e.g. a two-clause landing where the dash clause moved to its own
|
|
178
|
+
// line) — render them as breaks, not spaces.
|
|
179
|
+
white-space: pre-line;
|
|
180
|
+
overflow-wrap: anywhere;
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
.hk-error-landing__desc {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { createApp, h } from "vue";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, h, nextTick } from "vue";
|
|
3
3
|
|
|
4
4
|
import { HkErrorLanding } from "./HkErrorLanding";
|
|
5
5
|
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
@@ -11,6 +11,8 @@ interface MountOptions {
|
|
|
11
11
|
status?: number;
|
|
12
12
|
tone?: "error" | "warning" | "info";
|
|
13
13
|
variant?: "page" | "inline";
|
|
14
|
+
copyText?: string;
|
|
15
|
+
copyLabel?: string;
|
|
14
16
|
details?: () => ReturnType<typeof h>;
|
|
15
17
|
actions?: () => ReturnType<typeof h>;
|
|
16
18
|
brand?: () => ReturnType<typeof h>;
|
|
@@ -28,6 +30,8 @@ function mountLanding(opts: MountOptions = {}) {
|
|
|
28
30
|
status: opts.status,
|
|
29
31
|
tone: opts.tone ?? "error",
|
|
30
32
|
variant: opts.variant ?? "page",
|
|
33
|
+
copyText: opts.copyText ?? "",
|
|
34
|
+
copyLabel: opts.copyLabel ?? "",
|
|
31
35
|
}, {
|
|
32
36
|
...(opts.details ? { default: opts.details } : {}),
|
|
33
37
|
...(opts.actions ? { actions: opts.actions } : {}),
|
|
@@ -39,12 +43,29 @@ function mountLanding(opts: MountOptions = {}) {
|
|
|
39
43
|
return container;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/** Stub the async clipboard (jsdom ships none) and restore afterwards. */
|
|
47
|
+
function stubClipboard() {
|
|
48
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
49
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
50
|
+
value: { writeText },
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
});
|
|
54
|
+
return writeText;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function actionButtons(el: HTMLElement): HTMLButtonElement[] {
|
|
58
|
+
return Array.from(el.querySelectorAll<HTMLButtonElement>(".hk-error-landing__actions button"));
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
afterEach(() => {
|
|
43
62
|
for (const { app, container } of mounts) {
|
|
44
63
|
app.unmount();
|
|
45
64
|
container.remove();
|
|
46
65
|
}
|
|
47
66
|
mounts.length = 0;
|
|
67
|
+
Reflect.deleteProperty(navigator, "clipboard");
|
|
68
|
+
vi.restoreAllMocks();
|
|
48
69
|
});
|
|
49
70
|
|
|
50
71
|
describe("HkErrorLanding", () => {
|
|
@@ -142,4 +163,51 @@ describe("HkErrorLanding", () => {
|
|
|
142
163
|
const root = el.querySelector(".hk-error-landing")!;
|
|
143
164
|
expect(root.classList.contains("is-inline")).toBe(true);
|
|
144
165
|
});
|
|
166
|
+
|
|
167
|
+
it("seats the built-in copy action FIRST, left of the host actions", () => {
|
|
168
|
+
const el = mountLanding({
|
|
169
|
+
copyText: "Boom: bad",
|
|
170
|
+
actions: () => h("button", { class: "fake-action" }, "Retry"),
|
|
171
|
+
});
|
|
172
|
+
const buttons = actionButtons(el);
|
|
173
|
+
expect(buttons).toHaveLength(2);
|
|
174
|
+
expect(buttons[0]!.textContent).toBe("Copy error details");
|
|
175
|
+
expect(buttons[1]!.classList.contains("fake-action")).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("writes the copy payload and flips the label to Copied", async () => {
|
|
179
|
+
const writeText = stubClipboard();
|
|
180
|
+
const el = mountLanding({ copyText: "full error info" });
|
|
181
|
+
const copy = actionButtons(el)[0]!;
|
|
182
|
+
copy.click();
|
|
183
|
+
await vi.waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
|
|
184
|
+
expect(writeText).toHaveBeenCalledWith("full error info");
|
|
185
|
+
await nextTick();
|
|
186
|
+
await nextTick();
|
|
187
|
+
expect(actionButtons(el)[0]!.textContent).toBe("Copied");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("renders the copy action alone when the host passes no actions", () => {
|
|
191
|
+
const el = mountLanding({ copyText: "raw" });
|
|
192
|
+
const buttons = actionButtons(el);
|
|
193
|
+
expect(buttons).toHaveLength(1);
|
|
194
|
+
expect(buttons[0]!.textContent).toBe("Copy error details");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("renders no copy action and no actions row without copyText and slot", () => {
|
|
198
|
+
const el = mountLanding({ title: "Boom" });
|
|
199
|
+
expect(el.querySelector(".hk-error-landing__actions")).toBeNull();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("keeps the actions row host-only when copyText is empty", () => {
|
|
203
|
+
const el = mountLanding({ actions: () => h("button", { class: "fake-action" }, "Retry") });
|
|
204
|
+
const buttons = actionButtons(el);
|
|
205
|
+
expect(buttons).toHaveLength(1);
|
|
206
|
+
expect(buttons[0]!.classList.contains("fake-action")).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("honours a custom copy label override", () => {
|
|
210
|
+
const el = mountLanding({ copyText: "raw", copyLabel: "Copy Error" });
|
|
211
|
+
expect(actionButtons(el)[0]!.textContent).toBe("Copy Error");
|
|
212
|
+
});
|
|
145
213
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Braces, Info, TriangleAlert } from "lucide-vue-next";
|
|
1
|
+
import { Braces, Check, Copy, Info, TriangleAlert } from "lucide-vue-next";
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
defineComponent,
|
|
@@ -7,11 +7,14 @@ import {
|
|
|
7
7
|
onUpdated,
|
|
8
8
|
ref,
|
|
9
9
|
type PropType,
|
|
10
|
+
type VNode,
|
|
10
11
|
} from "vue";
|
|
11
12
|
|
|
12
13
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
13
14
|
import { useI18n } from "../i18n/context";
|
|
15
|
+
import { useClipboard } from "../runtime/useClipboard";
|
|
14
16
|
import HkBadge from "./HkBadge";
|
|
17
|
+
import HkButton from "./HkButton";
|
|
15
18
|
|
|
16
19
|
import "./HkErrorLanding.scss";
|
|
17
20
|
|
|
@@ -27,6 +30,13 @@ export type HErrorTone = "error" | "warning" | "info";
|
|
|
27
30
|
*/
|
|
28
31
|
export type HErrorLandingVariant = "page" | "inline";
|
|
29
32
|
|
|
33
|
+
/** Slot renderers may return a single vnode or an array — normalize to an
|
|
34
|
+
* array so the built-in copy action and the host's actions share one row. */
|
|
35
|
+
function normalizeSlotNodes(nodes: VNode | VNode[] | undefined): VNode[] {
|
|
36
|
+
if (!nodes) return [];
|
|
37
|
+
return Array.isArray(nodes) ? nodes : [nodes];
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
/**
|
|
31
41
|
* HkErrorLanding — the shared full-page error landing.
|
|
32
42
|
*
|
|
@@ -46,6 +56,15 @@ export type HErrorLandingVariant = "page" | "inline";
|
|
|
46
56
|
* server-rendered error page alike. All host-facing copy (`title`,
|
|
47
57
|
* `description`, action buttons) arrives pre-translated; the component only
|
|
48
58
|
* translates its own labels via `hikari::errors.*`.
|
|
59
|
+
*
|
|
60
|
+
* The standard copy action: hosts pass `copyText` (the full error info they
|
|
61
|
+
* want on the clipboard — headline, technical context, raw payload, …) and
|
|
62
|
+
* the landing seats its built-in "copy error details" button as the FIRST
|
|
63
|
+
* action, left of whatever the actions slot renders — one design for the
|
|
64
|
+
* whole error-landing family instead of every consumer hand-rolling its
|
|
65
|
+
* own copy row (the boundary used to ship a private one; it now feeds this
|
|
66
|
+
* hook). `copyLabel` overrides the button wording when a host already
|
|
67
|
+
* ships its own.
|
|
49
68
|
*/
|
|
50
69
|
export const HkErrorLanding = defineComponent({
|
|
51
70
|
name: "HkErrorLanding",
|
|
@@ -58,15 +77,29 @@ export const HkErrorLanding = defineComponent({
|
|
|
58
77
|
code: { type: String, default: "" },
|
|
59
78
|
/** HTTP status chip, e.g. 400. */
|
|
60
79
|
status: { type: Number, default: undefined },
|
|
80
|
+
/** Full error info the built-in copy action puts on the clipboard
|
|
81
|
+
* (headline + technical context + raw payload, host-composed).
|
|
82
|
+
* Empty disables the action. */
|
|
83
|
+
copyText: { type: String, default: "" },
|
|
84
|
+
/** Optional wording override for the built-in copy button; empty
|
|
85
|
+
* falls back to `hikari::errors.copyDetails`. */
|
|
86
|
+
copyLabel: { type: String, default: "" },
|
|
61
87
|
tone: { type: String as PropType<HErrorTone>, default: "error" },
|
|
62
88
|
/** Layout variant: `page` (viewport backdrop) or `inline` (in-flow card). */
|
|
63
89
|
variant: { type: String as PropType<HErrorLandingVariant>, default: "page" },
|
|
64
90
|
},
|
|
65
91
|
setup(props, { slots }) {
|
|
66
92
|
const { t } = useI18n();
|
|
93
|
+
const clipboard = useClipboard();
|
|
67
94
|
|
|
68
95
|
const titleText = computed(() => props.title || t("hikari::errors.defaultTitle", "Something went wrong"));
|
|
69
96
|
const hasDetails = computed(() => slots.default != null);
|
|
97
|
+
// The actions row carries the built-in copy action whenever the host
|
|
98
|
+
// feeds it a payload — with or without slot actions beside it.
|
|
99
|
+
// Frozen like hasDetails: the slots object identity is not reactive,
|
|
100
|
+
// so a slot appearing mid-lifetime wouldn't flip this row — every
|
|
101
|
+
// family host mounts the landing with its slot set already settled.
|
|
102
|
+
const hasActions = computed(() => slots.actions != null || props.copyText !== "");
|
|
70
103
|
|
|
71
104
|
// Badge variant follows the landing tone so the chip, the icon and the
|
|
72
105
|
// card wash always speak the same severity language.
|
|
@@ -175,7 +208,31 @@ export const HkErrorLanding = defineComponent({
|
|
|
175
208
|
</div>
|
|
176
209
|
)}
|
|
177
210
|
|
|
178
|
-
{
|
|
211
|
+
{hasActions.value && (
|
|
212
|
+
<div class="hk-error-landing__actions" aria-live="polite">
|
|
213
|
+
{[
|
|
214
|
+
// The standard copy action seats FIRST — left of every
|
|
215
|
+
// host-provided action — so the whole family reads
|
|
216
|
+
// [copy] [retry] [dismiss…] no matter who hosts the card.
|
|
217
|
+
...(props.copyText
|
|
218
|
+
? [(
|
|
219
|
+
<HkButton
|
|
220
|
+
key="hk-error-copy"
|
|
221
|
+
size="sm"
|
|
222
|
+
variant="ghost"
|
|
223
|
+
onClick={() => { void clipboard.copy(props.copyText); }}
|
|
224
|
+
>
|
|
225
|
+
{clipboard.copied.value ? <Check size={12} /> : <Copy size={12} />}
|
|
226
|
+
{clipboard.copied.value
|
|
227
|
+
? t("hikari::errors.copied", "Copied")
|
|
228
|
+
: props.copyLabel || t("hikari::errors.copyDetails", "Copy error details")}
|
|
229
|
+
</HkButton>
|
|
230
|
+
)]
|
|
231
|
+
: []),
|
|
232
|
+
...(normalizeSlotNodes(slots.actions?.())),
|
|
233
|
+
]}
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
179
236
|
</div>
|
|
180
237
|
</div>
|
|
181
238
|
);
|
|
@@ -179,7 +179,7 @@ export const HkLogWindow = defineComponent({
|
|
|
179
179
|
ref={scrollRef}
|
|
180
180
|
>
|
|
181
181
|
{currentLines.value.length === 0 ? (
|
|
182
|
-
<div class="s-log-empty">{t("hikari::log.empty", "No log lines yet
|
|
182
|
+
<div class="s-log-empty">{t("hikari::log.empty", "No log lines yet")}</div>
|
|
183
183
|
) : (
|
|
184
184
|
currentLines.value.map((line, i) => (
|
|
185
185
|
<div key={i} class={["s-log-entry", `s-log-entry-${levelOf(line)}`]}>
|
|
@@ -79,7 +79,7 @@ export const HkTokenUsagePanel = defineComponent({
|
|
|
79
79
|
<div class="s-token-panel">
|
|
80
80
|
{props.entries.length === 0 ? (
|
|
81
81
|
<div class="s-token-panel-empty">
|
|
82
|
-
{t("hikari::tokenUsage.noData", "No token usage data available
|
|
82
|
+
{t("hikari::tokenUsage.noData", "No token usage data available")}
|
|
83
83
|
</div>
|
|
84
84
|
) : (
|
|
85
85
|
<>
|
|
@@ -63,7 +63,7 @@ export const HkVoiceInputPopup = defineComponent({
|
|
|
63
63
|
{props.mode === "notConfigured" ? (
|
|
64
64
|
<div class="s-voice-popup-body" data-phase="install">
|
|
65
65
|
<p class="s-voice-popup-text">
|
|
66
|
-
{t("hikari::chat.voice_not_configured", "Voice input requires the Whisper service
|
|
66
|
+
{t("hikari::chat.voice_not_configured", "Voice input requires the Whisper service")}
|
|
67
67
|
</p>
|
|
68
68
|
<button class="s-voice-popup-link" type="button" onClick={openSettings}>
|
|
69
69
|
{t("hikari::chat.voice_go_settings", "Open Voice Settings →")}
|
|
@@ -18,7 +18,8 @@ import "./HkErrorReportingOverlay.scss";
|
|
|
18
18
|
* the family-wide HkErrorLanding (same design language as every unified
|
|
19
19
|
* error surface): tone icon, headline, the error name as the code chip,
|
|
20
20
|
* the message as the description, the raw error record in the fixed-height
|
|
21
|
-
* raw-details pane,
|
|
21
|
+
* raw-details pane, the landing's built-in copy action (fed the plain-text
|
|
22
|
+
* error record), and Home / Retry actions.
|
|
22
23
|
*
|
|
23
24
|
* Mounted by `createErrorReporting` on a dedicated root appended to
|
|
24
25
|
* `document.body` via its own tiny app instance, so it keeps working even
|
|
@@ -43,6 +44,18 @@ export const HkErrorReportingOverlay = defineComponent({
|
|
|
43
44
|
return record;
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
// Clipboard payload for the landing's built-in copy action: the same
|
|
48
|
+
// fields the raw-details pane shows, in plain-text form.
|
|
49
|
+
const copyText = computed(() => {
|
|
50
|
+
const err = state.value;
|
|
51
|
+
if (!err) return "";
|
|
52
|
+
const parts = [`${err.name}: ${err.message}`];
|
|
53
|
+
if (err.stack) parts.push("", err.stack);
|
|
54
|
+
if (err.source) parts.push("", `source: ${err.source}`);
|
|
55
|
+
if (err.info) parts.push("", `info: ${err.info}`);
|
|
56
|
+
return parts.join("\n");
|
|
57
|
+
});
|
|
58
|
+
|
|
46
59
|
function goHome() {
|
|
47
60
|
const options = getErrorReportingOptions();
|
|
48
61
|
if (options.onHome) {
|
|
@@ -69,7 +82,7 @@ export const HkErrorReportingOverlay = defineComponent({
|
|
|
69
82
|
|
|
70
83
|
const description = options.describe
|
|
71
84
|
? options.describe(err)
|
|
72
|
-
: err.message || t("hikari::errors.unexpectedDesc", "An unhandled error occurred
|
|
85
|
+
: err.message || t("hikari::errors.unexpectedDesc", "An unhandled error occurred");
|
|
73
86
|
|
|
74
87
|
const showHome = Boolean(options.onHome) || options.homeHref !== false;
|
|
75
88
|
|
|
@@ -79,6 +92,7 @@ export const HkErrorReportingOverlay = defineComponent({
|
|
|
79
92
|
title={options.title || t("hikari::errors.defaultTitle", "Something went wrong")}
|
|
80
93
|
description={description}
|
|
81
94
|
code={err.name}
|
|
95
|
+
copyText={copyText.value}
|
|
82
96
|
>
|
|
83
97
|
{{
|
|
84
98
|
default: () => <HkJsonTree value={detailsValue.value} ariaLabel="stack trace" />,
|
|
@@ -74,6 +74,8 @@ describe("createErrorReporting", () => {
|
|
|
74
74
|
|
|
75
75
|
const host = overlayHost()!;
|
|
76
76
|
const labels = Array.from(host.querySelectorAll("button")).map((b) => b.textContent);
|
|
77
|
+
// The built-in copy action seats first, left of Home / Retry.
|
|
78
|
+
expect(labels[0]).toBe("Copy error details");
|
|
77
79
|
expect(labels).toContain("Back to home");
|
|
78
80
|
expect(labels).toContain("Retry");
|
|
79
81
|
|
|
@@ -84,6 +86,7 @@ describe("createErrorReporting", () => {
|
|
|
84
86
|
await nextTick();
|
|
85
87
|
const labels2 = Array.from(overlayHost()!.querySelectorAll("button")).map((b) => b.textContent);
|
|
86
88
|
expect(labels2).not.toContain("Back to home");
|
|
89
|
+
expect(labels2).toContain("Copy error details");
|
|
87
90
|
expect(labels2).toContain("Retry");
|
|
88
91
|
});
|
|
89
92
|
|
|
@@ -21,7 +21,7 @@ import { describe, expect, it } from "vitest";
|
|
|
21
21
|
* (the reverse direction is what catches the key deleted from en only).
|
|
22
22
|
*/
|
|
23
23
|
const modules = import.meta.glob<{ default: Record<string, unknown> }>(
|
|
24
|
-
"./locales/*/components.json",
|
|
24
|
+
"./locales/*/{components,errors}.json",
|
|
25
25
|
{ eager: true },
|
|
26
26
|
);
|
|
27
27
|
|
|
@@ -59,18 +59,28 @@ const LOCALE_SPECIFIC_NESTED: Record<string, string[]> = {
|
|
|
59
59
|
"zh-Hant": ["components::hikari::statusBar.region.CN", "components::hikari::statusBar.region.HK", "components::hikari::statusBar.region.MO", "components::hikari::statusBar.region.TW"],
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
describe("
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
describe("locale bundle flat-key parity", () => {
|
|
63
|
+
// components.json AND errors.json merged per locale: the error
|
|
64
|
+
// landing's catalog rides the same silent-fallback failure mode (a
|
|
65
|
+
// key missing from one locale quietly renders English there), so the
|
|
66
|
+
// guard must see one bundle per locale, not one per file.
|
|
67
|
+
const bundles = EXPECTED_LOCALES.map((locale) => {
|
|
68
|
+
const merged: Record<string, unknown> = {};
|
|
69
|
+
for (const [path, mod] of Object.entries(modules)) {
|
|
70
|
+
if (path.includes(`/${locale}/`)) Object.assign(merged, mod.default);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
locale,
|
|
74
|
+
flat: Object.fromEntries(
|
|
75
|
+
Object.entries(merged).filter(([, v]) => typeof v === "string"),
|
|
76
|
+
) as Record<string, string>,
|
|
77
|
+
flatAll: Object.keys(merged).filter(
|
|
78
|
+
(k) => typeof merged[k] === "string",
|
|
79
|
+
),
|
|
80
|
+
nested: nestedStringLeaves(merged).leaves,
|
|
81
|
+
nonStringLeaves: nestedStringLeaves(merged).nonStringLeaves,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
74
84
|
|
|
75
85
|
it("covers all 11 locales", () => {
|
|
76
86
|
expect(bundles.map((b) => b.locale).sort()).toEqual(EXPECTED_LOCALES);
|
|
@@ -123,6 +133,11 @@ describe("components.json flat-key parity", () => {
|
|
|
123
133
|
expect(enKeys, "the passwordInput family is nested").toContain(
|
|
124
134
|
"components::hikari::passwordInput.strengthLabel",
|
|
125
135
|
);
|
|
136
|
+
// Self-check for the merged errors.json half: if the glob ever
|
|
137
|
+
// stops loading it, this guard would silently shrink to components.
|
|
138
|
+
expect(enKeys, "the errors catalog is nested").toContain(
|
|
139
|
+
"errors::hikari::errors.copyDetails",
|
|
140
|
+
);
|
|
126
141
|
for (const { locale, nested } of bundles) {
|
|
127
142
|
const allowed = LOCALE_SPECIFIC_NESTED[locale] ?? [];
|
|
128
143
|
const localeKeys = Object.keys(nested).sort();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"hikari::chat.send": "إرسال",
|
|
4
4
|
"hikari::chat.attachFile": "إرفاق ملف",
|
|
5
5
|
"hikari::chat.voiceInput": "إدخال صوتي",
|
|
6
|
-
"hikari::chat.voice_not_configured": "يتطلب الإدخال الصوتي خدمة Whisper
|
|
6
|
+
"hikari::chat.voice_not_configured": "يتطلب الإدخال الصوتي خدمة Whisper",
|
|
7
7
|
"hikari::chat.voice_go_settings": "فتح إعدادات الصوت ←",
|
|
8
8
|
"hikari::chat.listening": "جارٍ الاستماع… انقر للإيقاف",
|
|
9
9
|
"hikari::chat.transcribing": "جارٍ النسخ…",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"hikari::tools.waitingArgs": "بانتظار الوسائط…",
|
|
19
19
|
"hikari::tools.executing": "جارٍ التنفيذ…",
|
|
20
20
|
"hikari::tokenUsage.title": "استخدام الرموز",
|
|
21
|
-
"hikari::tokenUsage.noData": "لا توجد بيانات عن استخدام
|
|
21
|
+
"hikari::tokenUsage.noData": "لا توجد بيانات عن استخدام الرموز",
|
|
22
22
|
"hikari::tokenUsage.prompt": "الموجه",
|
|
23
23
|
"hikari::tokenUsage.completion": "الإكمال",
|
|
24
24
|
"hikari::tokenUsage.total": "الإجمالي",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"hikari::dateTimePicker.prevYears": "السنوات السابقة",
|
|
43
43
|
"hikari::dateTimePicker.today": "اليوم",
|
|
44
44
|
"hikari::drawer.close": "إغلاق",
|
|
45
|
-
"hikari::errorBoundary.copyError": "نسخ الخطأ",
|
|
46
45
|
"hikari::errorBoundary.retry": "إعادة المحاولة",
|
|
47
46
|
"hikari::errorBoundary.title": "خطأ في المكوّن",
|
|
48
47
|
"hikari::filePicker.browse": "استعراض…",
|
|
@@ -212,7 +211,7 @@
|
|
|
212
211
|
"hikari::affixPicker.remove": "إزالة",
|
|
213
212
|
"hikari::affixPicker.useCustom": "استخدام \"{query}\"",
|
|
214
213
|
"hikari::affixPicker.removeConfirmTitle": "إزالة العنصر",
|
|
215
|
-
"hikari::affixPicker.removeConfirm": "إزالة \"{label}\"؟ لا يمكن التراجع عن هذا
|
|
214
|
+
"hikari::affixPicker.removeConfirm": "إزالة \"{label}\"؟ لا يمكن التراجع عن هذا الإجراء",
|
|
216
215
|
"hikari::localizedInput.noMatches": "لا توجد لغة مطابقة",
|
|
217
216
|
"hikari::messageBox.confirm": "تأكيد",
|
|
218
217
|
"hikari::messageBox.cancel": "إلغاء",
|
|
@@ -230,7 +229,7 @@
|
|
|
230
229
|
"hikari::tagInput.addCustom": "استخدام \"{q}\"",
|
|
231
230
|
"hikari::tagInput.reorder": "إعادة الترتيب — {label}",
|
|
232
231
|
"hikari::modal.hiddenLayers": "الطبقات المخفية",
|
|
233
|
-
"hikari::theme.modeTabHint": "هذه التبويبات تختار لوحة الألوان التي تحرّرها؛ لتبديل الوضع الفاتح/الداكن للتطبيق كله استخدم شريط الأوضاع أعلى نافذة
|
|
232
|
+
"hikari::theme.modeTabHint": "هذه التبويبات تختار لوحة الألوان التي تحرّرها؛ لتبديل الوضع الفاتح/الداكن للتطبيق كله استخدم شريط الأوضاع أعلى نافذة السمة",
|
|
234
233
|
"hikari::theme.defaultThemeName": "افتراضي",
|
|
235
234
|
"hikari::emptyState.loading": "جارٍ التحميل",
|
|
236
235
|
"hikari::mfaVerify.title": "التحقق من الحساب",
|
|
@@ -3,7 +3,6 @@ hk.adminHeader.logout = "Logout"
|
|
|
3
3
|
hk.adminHeader.login = "Login"
|
|
4
4
|
hk.adminHeader.toggleTheme = "Toggle theme"
|
|
5
5
|
hk.errorBoundary.title = "Component Error"
|
|
6
|
-
hk.errorBoundary.copyError = "Copy Error"
|
|
7
6
|
hk.errorBoundary.retry = "Retry"
|
|
8
7
|
hk.searchInput.placeholder = "Search…"
|
|
9
8
|
hk.searchInput.clear = "Clear search"
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"hikari::errors.defaultTitle": "حدث خطأ ما",
|
|
4
4
|
"hikari::errors.rawDetails": "تفاصيل الخطأ الأصلية",
|
|
5
5
|
"hikari::errors.backHome": "العودة إلى الرئيسية",
|
|
6
|
-
"hikari::errors.unexpectedDesc": "واجه التطبيق خطأً غير معالج. يمكنك العودة إلى الصفحة الرئيسية أو إعادة
|
|
6
|
+
"hikari::errors.unexpectedDesc": "واجه التطبيق خطأً غير معالج. يمكنك العودة إلى الصفحة الرئيسية أو إعادة المحاولة",
|
|
7
|
+
"hikari::errors.copyDetails": "نسخ تفاصيل الخطأ",
|
|
8
|
+
"hikari::errors.copied": "تم النسخ"
|
|
7
9
|
}
|
|
8
10
|
}
|