@celestia-island/hikari 0.55.132 → 0.55.133
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/animation/registerAnimations.ts +6 -0
- package/src/components/HkPersistentToast.scss +143 -0
- package/src/components/HkPersistentToast.test.tsx +263 -0
- package/src/components/HkPersistentToast.tsx +290 -0
- package/src/components/HkPersistentToastGroup.scss +22 -0
- package/src/components/HkPersistentToastGroup.test.tsx +82 -0
- package/src/components/HkPersistentToastGroup.tsx +45 -0
- package/src/i18n/locales/ar/components.json +1 -0
- package/src/i18n/locales/de/components.json +1 -0
- package/src/i18n/locales/en/components.json +1 -0
- package/src/i18n/locales/es/components.json +1 -0
- package/src/i18n/locales/fr/components.json +1 -0
- package/src/i18n/locales/ja/components.json +1 -0
- package/src/i18n/locales/ko/components.json +1 -0
- package/src/i18n/locales/pt/components.json +1 -0
- package/src/i18n/locales/ru/components.json +1 -0
- package/src/i18n/locales/zh-Hans/components.json +1 -0
- package/src/i18n/locales/zh-Hant/components.json +1 -0
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -119,6 +119,12 @@ registerCssAnimation("hk-placeholder-marquee-scroll", { infinite: true });
|
|
|
119
119
|
registerCssAnimation("s-admin-header-emergency-pulse", { infinite: true });
|
|
120
120
|
registerCssAnimation("hk-loading-veil-in");
|
|
121
121
|
registerCssAnimation("hk-loading-veil-spin", { infinite: true });
|
|
122
|
+
// components/HkPersistentToast.scss — chrome-bar chip spinner (loops
|
|
123
|
+
// forever while the loading tone shows).
|
|
124
|
+
registerCssAnimation("hk-persistent-toast-spin", { infinite: true });
|
|
125
|
+
// components/HkPersistentToast.scss — hover detail card entrance (plays
|
|
126
|
+
// once; hides instantly on close, so no leave keyframe).
|
|
127
|
+
registerCssAnimation("hk-persistent-toast-card-in");
|
|
122
128
|
// The styles/theme/mixins.scss ambient/glow/entrance keyframes were
|
|
123
129
|
// retired with the legacy Gen-1 sheet vendoring (hikari #476) — their
|
|
124
130
|
// registrar entries went with them.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Chrome-bar persistent status chip — proportions match a title-bar
|
|
2
|
+
// loading pill (HkStatusPill's compactness at toast-family semantics).
|
|
3
|
+
// All colors ride the RGB-channel theme tokens so light/dark and the
|
|
4
|
+
// glass/opacity pipelines apply without component-side branching.
|
|
5
|
+
|
|
6
|
+
.hk-persistent-toast-wrap {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 2px;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
// The wrap owns the live-region semantics; the chip inside may be a
|
|
12
|
+
// button (chrome no-drag regions only exempt buttons from window
|
|
13
|
+
// dragging — see the wowsp title bar's interactive() gate).
|
|
14
|
+
font-size: var(--text-2xs, 0.6875rem);
|
|
15
|
+
line-height: 1.4;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.hk-persistent-toast {
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--space-6, 0.375rem);
|
|
22
|
+
padding: 2px var(--space-8, 0.5rem);
|
|
23
|
+
border-radius: var(--radius-full, 999px);
|
|
24
|
+
border: 1px solid var(--border-faint, rgb(var(--color-border) / 10%));
|
|
25
|
+
background: rgb(var(--color-surface) / 60%);
|
|
26
|
+
color: rgb(var(--color-text) / 72%);
|
|
27
|
+
font: inherit;
|
|
28
|
+
max-width: 220px;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Interactive variant (detail slot provided): a real button — reset the
|
|
33
|
+
// browser chrome, tighten the start padding so the icon optically
|
|
34
|
+
// balances, and give the hover a faint lift like HkTag's interactive
|
|
35
|
+
// cousins.
|
|
36
|
+
button.hk-persistent-toast {
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
appearance: none;
|
|
39
|
+
padding-inline-start: var(--space-6, 0.375rem);
|
|
40
|
+
|
|
41
|
+
&:hover {
|
|
42
|
+
background: rgb(var(--color-surface) / 85%);
|
|
43
|
+
color: rgb(var(--color-text) / 90%);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&:focus-visible {
|
|
47
|
+
outline: 1px solid rgb(var(--color-primary) / 70%);
|
|
48
|
+
outline-offset: 1px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.hk-persistent-toast__icon {
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hk-persistent-toast--warning .hk-persistent-toast__icon {
|
|
57
|
+
color: rgb(var(--color-warning));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.hk-persistent-toast--error .hk-persistent-toast__icon {
|
|
61
|
+
color: rgb(var(--color-error));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.hk-persistent-toast--info .hk-persistent-toast__icon {
|
|
65
|
+
color: rgb(var(--color-muted));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.hk-persistent-toast__label {
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
text-overflow: ellipsis;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Same grammar as HkToast's spinner, sized for the chip and themed via
|
|
75
|
+
// currentColor so the tone classes stay in one place.
|
|
76
|
+
.hk-persistent-toast__spinner {
|
|
77
|
+
width: 11px;
|
|
78
|
+
height: 11px;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
border: 2px solid rgb(var(--color-text) / 25%);
|
|
81
|
+
border-top-color: rgb(var(--color-text) / 80%);
|
|
82
|
+
border-radius: 50%;
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
animation: hk-persistent-toast-spin 0.7s linear infinite;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes hk-persistent-toast-spin {
|
|
88
|
+
to {
|
|
89
|
+
transform: rotate(360deg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hk-persistent-toast__dismiss {
|
|
94
|
+
appearance: none;
|
|
95
|
+
border: none;
|
|
96
|
+
background: transparent;
|
|
97
|
+
color: rgb(var(--color-text) / 45%);
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
padding: 2px;
|
|
102
|
+
border-radius: var(--radius-full, 999px);
|
|
103
|
+
|
|
104
|
+
&:hover {
|
|
105
|
+
color: rgb(var(--color-text));
|
|
106
|
+
background: rgb(var(--color-text) / 8%);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:focus-visible {
|
|
110
|
+
outline: 1px solid rgb(var(--color-primary) / 70%);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The detail card: a fixed-position rich surface teleported to body,
|
|
115
|
+
// positioned by the component (below the chip, end-aligned, viewport-
|
|
116
|
+
// clamped). Glass surface rides the shared hii-dropdown-content class;
|
|
117
|
+
// only geometry and motion live here.
|
|
118
|
+
.hk-persistent-toast-card {
|
|
119
|
+
position: fixed;
|
|
120
|
+
min-width: 200px;
|
|
121
|
+
max-width: 340px;
|
|
122
|
+
padding: var(--space-8, 0.5rem) var(--space-10, 0.75rem);
|
|
123
|
+
border-radius: var(--radius-lg, 0.5rem);
|
|
124
|
+
font-size: var(--text-2xs, 0.6875rem);
|
|
125
|
+
line-height: 1.5;
|
|
126
|
+
color: rgb(var(--color-text));
|
|
127
|
+
font-family: inherit;
|
|
128
|
+
animation: hk-persistent-toast-card-in 120ms ease-out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@keyframes hk-persistent-toast-card-in {
|
|
132
|
+
from {
|
|
133
|
+
opacity: 0;
|
|
134
|
+
transform: translateY(-4px);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@media (prefers-reduced-motion: reduce) {
|
|
139
|
+
.hk-persistent-toast__spinner,
|
|
140
|
+
.hk-persistent-toast-card {
|
|
141
|
+
animation: none;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h, nextTick } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkPersistentToast from "./HkPersistentToast";
|
|
5
|
+
import { POPUP_Z_BANDS, usePopupManager } from "../runtime/usePopupManager";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HkPersistentToast contract tests:
|
|
9
|
+
* - inert default: a chip without a detail slot renders a plain span,
|
|
10
|
+
* engages no popup-manager entry and exposes no aria-expanded
|
|
11
|
+
* - hover open/close: detail slot + chip mouseenter opens the rich card
|
|
12
|
+
* after the open delay (one tooltip-kind band entry, z stamped
|
|
13
|
+
* inline); chip mouseleave closes it after the grace; moving onto the
|
|
14
|
+
* card cancels the close (hover continuity is the whole point of the
|
|
15
|
+
* rich card — the host list must survive the trip across the gap)
|
|
16
|
+
* - click pinning: a click pins the card against the grace timer until
|
|
17
|
+
* an outside pointerdown / Escape / another click closes it
|
|
18
|
+
* - dismiss: the ✕ sibling button emits `dismiss` (never nested inside
|
|
19
|
+
* the chip button)
|
|
20
|
+
* - the wrap carries the live-region semantics (role=status +
|
|
21
|
+
* aria-live=polite) so label changes announce
|
|
22
|
+
*
|
|
23
|
+
* House style: raw createApp mounts on a shared container list torn
|
|
24
|
+
* down after each case; the popup-manager singleton is REAL state,
|
|
25
|
+
* swept afterEach the way HkTooltip.test.tsx does.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
29
|
+
|
|
30
|
+
interface MountOptions {
|
|
31
|
+
props?: Record<string, unknown>;
|
|
32
|
+
detail?: string;
|
|
33
|
+
onDismiss?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mount({ props = {}, detail, onDismiss }: MountOptions = {}) {
|
|
37
|
+
const container = document.createElement("div");
|
|
38
|
+
document.body.appendChild(container);
|
|
39
|
+
const host = defineComponent({
|
|
40
|
+
setup() {
|
|
41
|
+
return () =>
|
|
42
|
+
h(
|
|
43
|
+
HkPersistentToast,
|
|
44
|
+
{ ...props, onDismiss } as never,
|
|
45
|
+
detail ? { detail: () => h("p", { class: "card-probe" }, detail) } : undefined,
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const app = createApp(host);
|
|
50
|
+
app.mount(container);
|
|
51
|
+
mounts.push({ app, container });
|
|
52
|
+
return { app, container };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const manager = usePopupManager();
|
|
56
|
+
|
|
57
|
+
function tooltipEntries() {
|
|
58
|
+
return [...manager.registry.value.values()].filter((e) => e.kind === "tooltip");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function chip(c: HTMLElement): HTMLElement {
|
|
62
|
+
return c.querySelector<HTMLElement>(".hk-persistent-toast")!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function wrap(c: HTMLElement): HTMLElement {
|
|
66
|
+
return c.querySelector<HTMLElement>(".hk-persistent-toast-wrap")!;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function card(): HTMLElement {
|
|
70
|
+
return document.querySelector<HTMLElement>(".hk-persistent-toast-card")!;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** real-timer settle: covers the open/close setTimeout + the rAF position pass */
|
|
74
|
+
async function settle() {
|
|
75
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
76
|
+
await nextTick();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fire(el: Element, type: string, Ctor: typeof MouseEvent = MouseEvent) {
|
|
80
|
+
el.dispatchEvent(new Ctor(type, { bubbles: true }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
86
|
+
app.unmount();
|
|
87
|
+
container.remove();
|
|
88
|
+
}
|
|
89
|
+
for (const entry of [...manager.registry.value.values()]) {
|
|
90
|
+
manager.unregister(entry.id);
|
|
91
|
+
}
|
|
92
|
+
document.querySelectorAll(".hk-persistent-toast-card").forEach((el) => el.remove());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("HkPersistentToast inert default", () => {
|
|
96
|
+
it("renders label, tone class and live-region semantics as a plain span", () => {
|
|
97
|
+
const { container } = mount({ props: { tone: "error", label: "Upstream fault" } });
|
|
98
|
+
const c = chip(container);
|
|
99
|
+
expect(c.tagName).toBe("SPAN");
|
|
100
|
+
expect(c.classList.contains("hk-persistent-toast--error")).toBe(true);
|
|
101
|
+
expect(c.textContent).toContain("Upstream fault");
|
|
102
|
+
expect(wrap(container).getAttribute("role")).toBe("status");
|
|
103
|
+
expect(wrap(container).getAttribute("aria-live")).toBe("polite");
|
|
104
|
+
expect(c.getAttribute("aria-expanded")).toBeNull();
|
|
105
|
+
expect(tooltipEntries().length).toBe(0);
|
|
106
|
+
expect(document.querySelector(".hk-persistent-toast-card")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders the spinner for the loading tone", () => {
|
|
110
|
+
const { container } = mount({ props: { tone: "loading", label: "Loading" } });
|
|
111
|
+
expect(container.querySelector(".hk-persistent-toast__spinner")).not.toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("renders the ✕ sibling button only when dismissible", () => {
|
|
115
|
+
const onDismiss = vi.fn();
|
|
116
|
+
const { container } = mount({ props: { label: "x", dismissible: true }, onDismiss });
|
|
117
|
+
const dismiss = container.querySelector<HTMLElement>(".hk-persistent-toast__dismiss")!;
|
|
118
|
+
expect(dismiss).not.toBeNull();
|
|
119
|
+
// The ✕ must never nest inside the chip (a button in a button is
|
|
120
|
+
// invalid markup).
|
|
121
|
+
expect(chip(container).contains(dismiss)).toBe(false);
|
|
122
|
+
dismiss.click();
|
|
123
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renders no ✕ when not dismissible", () => {
|
|
127
|
+
const { container } = mount({ props: { label: "x" } });
|
|
128
|
+
expect(container.querySelector(".hk-persistent-toast__dismiss")).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("HkPersistentToast detail card", () => {
|
|
133
|
+
it("opens on hover after the delay and closes after the grace", async () => {
|
|
134
|
+
const { container } = mount({
|
|
135
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0, detailLabel: "Failing hosts" },
|
|
136
|
+
detail: "vortex.wowsgame.cn",
|
|
137
|
+
});
|
|
138
|
+
fire(chip(container), "mouseenter");
|
|
139
|
+
await settle();
|
|
140
|
+
const c = card();
|
|
141
|
+
expect(c).not.toBeNull();
|
|
142
|
+
expect(c.textContent).toContain("vortex.wowsgame.cn");
|
|
143
|
+
expect(chip(container).getAttribute("aria-expanded")).toBe("true");
|
|
144
|
+
const entries = tooltipEntries();
|
|
145
|
+
expect(entries.length).toBe(1);
|
|
146
|
+
expect(entries[0]!.kind).toBe("tooltip");
|
|
147
|
+
expect(c.style.zIndex).toBe(String(POPUP_Z_BANDS.tooltip));
|
|
148
|
+
|
|
149
|
+
fire(chip(container), "mouseleave");
|
|
150
|
+
await settle();
|
|
151
|
+
expect(card()).toBeNull();
|
|
152
|
+
expect(tooltipEntries().length).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("delays the hover open by openDelay", async () => {
|
|
156
|
+
const { container } = mount({
|
|
157
|
+
props: { label: "fault", openDelay: 80, closeGrace: 0 },
|
|
158
|
+
detail: "hosts",
|
|
159
|
+
});
|
|
160
|
+
fire(chip(container), "mouseenter");
|
|
161
|
+
await nextTick();
|
|
162
|
+
expect(card()).toBeNull();
|
|
163
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
164
|
+
await nextTick();
|
|
165
|
+
expect(card()).not.toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("keeps the card open while the pointer moves onto it", async () => {
|
|
169
|
+
const { container } = mount({
|
|
170
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
171
|
+
detail: "hosts",
|
|
172
|
+
});
|
|
173
|
+
fire(chip(container), "mouseenter");
|
|
174
|
+
await settle();
|
|
175
|
+
// Chip → card crossing: the close grace must be cancelled by the
|
|
176
|
+
// card's own mouseenter, or the host list dies mid-read.
|
|
177
|
+
fire(chip(container), "mouseleave");
|
|
178
|
+
fire(card(), "mouseenter");
|
|
179
|
+
await settle();
|
|
180
|
+
expect(card()).not.toBeNull();
|
|
181
|
+
// Leaving the card closes (nothing pinned).
|
|
182
|
+
fire(card(), "mouseleave");
|
|
183
|
+
await settle();
|
|
184
|
+
expect(card()).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("pins on click: grace timer and chip leave cannot close it", async () => {
|
|
188
|
+
const { container } = mount({
|
|
189
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
190
|
+
detail: "hosts",
|
|
191
|
+
});
|
|
192
|
+
fire(chip(container), "mouseenter");
|
|
193
|
+
await settle();
|
|
194
|
+
chip(container).click();
|
|
195
|
+
fire(chip(container), "mouseleave");
|
|
196
|
+
await settle();
|
|
197
|
+
expect(card()).not.toBeNull();
|
|
198
|
+
expect(chip(container).getAttribute("aria-expanded")).toBe("true");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("an outside pointerdown closes a pinned card", async () => {
|
|
202
|
+
const { container } = mount({
|
|
203
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
204
|
+
detail: "hosts",
|
|
205
|
+
});
|
|
206
|
+
chip(container).click();
|
|
207
|
+
await settle();
|
|
208
|
+
expect(card()).not.toBeNull();
|
|
209
|
+
document.body.dispatchEvent(
|
|
210
|
+
new MouseEvent("pointerdown", { bubbles: true }),
|
|
211
|
+
);
|
|
212
|
+
await settle();
|
|
213
|
+
expect(card()).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("a click inside the card does not close a pinned card", async () => {
|
|
217
|
+
const { container } = mount({
|
|
218
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
219
|
+
detail: "hosts",
|
|
220
|
+
});
|
|
221
|
+
chip(container).click();
|
|
222
|
+
await settle();
|
|
223
|
+
card().dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
|
224
|
+
await settle();
|
|
225
|
+
expect(card()).not.toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("Escape closes a pinned card", async () => {
|
|
229
|
+
const { container } = mount({
|
|
230
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
231
|
+
detail: "hosts",
|
|
232
|
+
});
|
|
233
|
+
chip(container).click();
|
|
234
|
+
await settle();
|
|
235
|
+
chip(container).dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
236
|
+
await settle();
|
|
237
|
+
expect(card()).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("a second click toggles a pinned card closed", async () => {
|
|
241
|
+
const { container } = mount({
|
|
242
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
243
|
+
detail: "hosts",
|
|
244
|
+
});
|
|
245
|
+
chip(container).click();
|
|
246
|
+
await settle();
|
|
247
|
+
chip(container).click();
|
|
248
|
+
await settle();
|
|
249
|
+
expect(card()).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("releases the band entry on unmount while open", async () => {
|
|
253
|
+
const { app, container } = mount({
|
|
254
|
+
props: { label: "fault", openDelay: 0, closeGrace: 0 },
|
|
255
|
+
detail: "hosts",
|
|
256
|
+
});
|
|
257
|
+
fire(chip(container), "mouseenter");
|
|
258
|
+
await settle();
|
|
259
|
+
expect(tooltipEntries().length).toBe(1);
|
|
260
|
+
app.unmount();
|
|
261
|
+
expect(tooltipEntries().length).toBe(0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { AlertTriangle, Info, X } from "lucide-vue-next";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineComponent,
|
|
5
|
+
nextTick,
|
|
6
|
+
onBeforeUnmount,
|
|
7
|
+
ref,
|
|
8
|
+
Teleport,
|
|
9
|
+
type PropType,
|
|
10
|
+
type SlotsType,
|
|
11
|
+
} from "vue";
|
|
12
|
+
|
|
13
|
+
import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
14
|
+
import { useI18n } from "../i18n/context";
|
|
15
|
+
import "./HkPersistentToast.scss";
|
|
16
|
+
|
|
17
|
+
export type PersistentToastTone = "loading" | "info" | "warning" | "error";
|
|
18
|
+
|
|
19
|
+
/** Viewport gutter the detail card clamps into (mirror of
|
|
20
|
+
* --viewport-gutter's desktop value; the card is chrome-adjacent and
|
|
21
|
+
* small, so the mobile/desktop split is not worth a runtime read). */
|
|
22
|
+
const GUTTER_PX = 8;
|
|
23
|
+
/** Vertical gap between the chip's bottom edge and the detail card. */
|
|
24
|
+
const CARD_GAP_PX = 6;
|
|
25
|
+
|
|
26
|
+
let popupSeq = 0;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* HkPersistentToast — a persistent status chip for chrome bars (title
|
|
30
|
+
* bars, status strips): the third member of the toast family after
|
|
31
|
+
* HkToast (transient corner stack) and HkBlockingToast (window-modal).
|
|
32
|
+
* It never auto-dismisses; the host renders it while a condition holds
|
|
33
|
+
* (a load in flight, an upstream outage) and unmounts it when the
|
|
34
|
+
* condition clears.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <HPersistentToast tone="loading" label="Loading stats…" />
|
|
38
|
+
* <HPersistentToast tone="error" label="Upstream fault" onDismiss={silence}>
|
|
39
|
+
* <template #detail>…rich card…</template>
|
|
40
|
+
* </HPersistentToast>
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Behavior contract:
|
|
44
|
+
* - `tone="loading"` renders a spinner, the other tones a static icon;
|
|
45
|
+
* the label truncates, the chip never wraps.
|
|
46
|
+
* - With a `detail` slot the chip becomes a button: hover (after
|
|
47
|
+
* `openDelay`) or focus opens the rich card anchored below the chip;
|
|
48
|
+
* moving the pointer onto the card keeps it open (`closeGrace`), and
|
|
49
|
+
* a click PINNS it — a pinned card only closes on its own Escape /
|
|
50
|
+
* outside click / another chip click, so touch users get parity.
|
|
51
|
+
* - `onDismiss` (a dismiss emit) renders the ✕ affordance beside the
|
|
52
|
+
* chip; the chip itself stays a single interactive surface (the ✕ is
|
|
53
|
+
* a sibling button, never nested — a button inside a button is dead
|
|
54
|
+
* markup).
|
|
55
|
+
* - The wrap carries role="status" + aria-live="polite", so label
|
|
56
|
+
* changes announce without stealing focus — the property the wowsp
|
|
57
|
+
* title-bar loading chip relies on.
|
|
58
|
+
*/
|
|
59
|
+
export default defineComponent({
|
|
60
|
+
name: "HkPersistentToast",
|
|
61
|
+
props: {
|
|
62
|
+
tone: { type: String as PropType<PersistentToastTone>, default: "info" },
|
|
63
|
+
/** Text beside the icon; truncates rather than wraps. */
|
|
64
|
+
label: { type: String, default: "" },
|
|
65
|
+
/** Hover/focus card content; providing the slot makes the chip interactive. */
|
|
66
|
+
detailLabel: { type: String, default: "" },
|
|
67
|
+
/** Renders the ✕ affordance beside the chip; clicking it emits `dismiss`.
|
|
68
|
+
* A sibling button, never a child of the chip — a button inside a
|
|
69
|
+
* button is invalid markup and would break the chip's click toggle. */
|
|
70
|
+
dismissible: { type: Boolean, default: false },
|
|
71
|
+
openDelay: { type: Number, default: 150 },
|
|
72
|
+
closeGrace: { type: Number, default: 250 },
|
|
73
|
+
},
|
|
74
|
+
emits: {
|
|
75
|
+
dismiss: () => true,
|
|
76
|
+
},
|
|
77
|
+
slots: Object as SlotsType<{
|
|
78
|
+
detail?: () => unknown;
|
|
79
|
+
}>,
|
|
80
|
+
setup(props, { slots, emit }) {
|
|
81
|
+
const { t } = useI18n();
|
|
82
|
+
const manager = usePopupManager();
|
|
83
|
+
const wrapRef = ref<HTMLElement | null>(null);
|
|
84
|
+
const cardRef = ref<HTMLElement | null>(null);
|
|
85
|
+
const open = ref(false);
|
|
86
|
+
const pinned = ref(false);
|
|
87
|
+
const zIndex = ref<number | null>(null);
|
|
88
|
+
const popupId = `hk-persistent-toast-${++popupSeq}`;
|
|
89
|
+
let handle: PopupHandle | null = null;
|
|
90
|
+
let openTimer: ReturnType<typeof setTimeout> | null = null;
|
|
91
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
92
|
+
|
|
93
|
+
const interactive = computed(() => !!slots.detail);
|
|
94
|
+
|
|
95
|
+
// The card is LAZY (tooltip-bridge contract): a chip nobody hovers
|
|
96
|
+
// costs no teleported node and no band registration. The handle is
|
|
97
|
+
// held for the whole open window — hover-out only closes, unmount
|
|
98
|
+
// releases.
|
|
99
|
+
function ensureHandle() {
|
|
100
|
+
if (!handle) {
|
|
101
|
+
handle = manager.register("tooltip", false);
|
|
102
|
+
zIndex.value = handle.zIndex;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function releaseHandle() {
|
|
107
|
+
if (handle) {
|
|
108
|
+
manager.unregister(handle.id);
|
|
109
|
+
handle = null;
|
|
110
|
+
}
|
|
111
|
+
zIndex.value = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearTimers() {
|
|
115
|
+
if (openTimer !== null) {
|
|
116
|
+
clearTimeout(openTimer);
|
|
117
|
+
openTimer = null;
|
|
118
|
+
}
|
|
119
|
+
if (closeTimer !== null) {
|
|
120
|
+
clearTimeout(closeTimer);
|
|
121
|
+
closeTimer = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Anchor the card below the chip, right-aligned to it (chrome bars
|
|
126
|
+
* hug the window's trailing corner, so an end-aligned card grows
|
|
127
|
+
* leftward into the roomier side), clamped into the viewport. */
|
|
128
|
+
function positionCard() {
|
|
129
|
+
const chip = wrapRef.value;
|
|
130
|
+
const card = cardRef.value;
|
|
131
|
+
if (!chip || !card) return;
|
|
132
|
+
const rect = chip.getBoundingClientRect();
|
|
133
|
+
const vw = window.innerWidth;
|
|
134
|
+
const vh = window.innerHeight;
|
|
135
|
+
let left = rect.right - card.offsetWidth;
|
|
136
|
+
left = Math.max(GUTTER_PX, Math.min(left, vw - GUTTER_PX - card.offsetWidth));
|
|
137
|
+
let top = rect.bottom + CARD_GAP_PX;
|
|
138
|
+
top = Math.max(GUTTER_PX, Math.min(top, vh - GUTTER_PX - card.offsetHeight));
|
|
139
|
+
card.style.left = `${left}px`;
|
|
140
|
+
card.style.top = `${top}px`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function openCard(pin: boolean) {
|
|
144
|
+
clearTimers();
|
|
145
|
+
ensureHandle();
|
|
146
|
+
if (pin) pinned.value = true;
|
|
147
|
+
open.value = true;
|
|
148
|
+
nextTick(() => requestAnimationFrame(positionCard));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function closeCard(force = false) {
|
|
152
|
+
if (pinned.value && !force) return;
|
|
153
|
+
pinned.value = false;
|
|
154
|
+
open.value = false;
|
|
155
|
+
releaseHandle();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function scheduleOpen() {
|
|
159
|
+
if (openTimer !== null) clearTimeout(openTimer);
|
|
160
|
+
openTimer = setTimeout(() => {
|
|
161
|
+
openTimer = null;
|
|
162
|
+
openCard(false);
|
|
163
|
+
}, props.openDelay);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scheduleClose() {
|
|
167
|
+
if (closeTimer !== null) clearTimeout(closeTimer);
|
|
168
|
+
closeTimer = setTimeout(() => {
|
|
169
|
+
closeTimer = null;
|
|
170
|
+
closeCard(false);
|
|
171
|
+
}, props.closeGrace);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cancelClose() {
|
|
175
|
+
if (closeTimer !== null) {
|
|
176
|
+
clearTimeout(closeTimer);
|
|
177
|
+
closeTimer = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onChipClick() {
|
|
182
|
+
if (pinned.value) {
|
|
183
|
+
closeCard(true);
|
|
184
|
+
} else {
|
|
185
|
+
// A click during the hover-open window promotes to pinned in
|
|
186
|
+
// place — the card must not flicker closed and reopen.
|
|
187
|
+
openCard(true);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function onChipKeydown(e: KeyboardEvent) {
|
|
192
|
+
if (e.key === "Escape" && open.value) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
closeCard(true);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Outside clicks close a PINNED card (a hover-open card closes on
|
|
200
|
+
// its own grace timer; listening always would fight the chip's own
|
|
201
|
+
// click toggle — hence the contains check).
|
|
202
|
+
function onDocumentPointerdown(e: PointerEvent) {
|
|
203
|
+
if (!open.value || !pinned.value) return;
|
|
204
|
+
const target = e.target as Node;
|
|
205
|
+
if (wrapRef.value?.contains(target)) return;
|
|
206
|
+
if (cardRef.value?.contains(target)) return;
|
|
207
|
+
closeCard(true);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function onWindowResize() {
|
|
211
|
+
if (open.value) positionCard();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
document.addEventListener("pointerdown", onDocumentPointerdown, true);
|
|
215
|
+
window.addEventListener("resize", onWindowResize);
|
|
216
|
+
|
|
217
|
+
onBeforeUnmount(() => {
|
|
218
|
+
clearTimers();
|
|
219
|
+
releaseHandle();
|
|
220
|
+
document.removeEventListener("pointerdown", onDocumentPointerdown, true);
|
|
221
|
+
window.removeEventListener("resize", onWindowResize);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const cardStyle = computed(() => (zIndex.value != null ? { zIndex: zIndex.value } : {}));
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
const Tag = (interactive.value ? "button" : "span") as "button";
|
|
228
|
+
return (
|
|
229
|
+
<span ref={wrapRef} class="hk-persistent-toast-wrap" role="status" aria-live="polite">
|
|
230
|
+
<Tag
|
|
231
|
+
type={interactive.value ? "button" : undefined}
|
|
232
|
+
class={["hk-persistent-toast", `hk-persistent-toast--${props.tone}`]}
|
|
233
|
+
aria-expanded={interactive.value ? open.value : undefined}
|
|
234
|
+
aria-haspopup={interactive.value ? "true" : undefined}
|
|
235
|
+
aria-controls={open.value ? popupId : undefined}
|
|
236
|
+
onMouseenter={interactive.value ? scheduleOpen : undefined}
|
|
237
|
+
onMouseleave={interactive.value ? scheduleClose : undefined}
|
|
238
|
+
onFocusin={interactive.value ? () => openCard(false) : undefined}
|
|
239
|
+
onFocusout={
|
|
240
|
+
interactive.value
|
|
241
|
+
? () => {
|
|
242
|
+
if (!pinned.value) closeCard(false);
|
|
243
|
+
}
|
|
244
|
+
: undefined
|
|
245
|
+
}
|
|
246
|
+
onClick={interactive.value ? onChipClick : undefined}
|
|
247
|
+
onKeydown={interactive.value ? onChipKeydown : undefined}
|
|
248
|
+
>
|
|
249
|
+
{props.tone === "loading" ? (
|
|
250
|
+
<span class="hk-persistent-toast__spinner" aria-hidden="true" />
|
|
251
|
+
) : props.tone === "error" || props.tone === "warning" ? (
|
|
252
|
+
<AlertTriangle size={12} class="hk-persistent-toast__icon" aria-hidden="true" />
|
|
253
|
+
) : (
|
|
254
|
+
<Info size={12} class="hk-persistent-toast__icon" aria-hidden="true" />
|
|
255
|
+
)}
|
|
256
|
+
<span class="hk-persistent-toast__label">{props.label}</span>
|
|
257
|
+
</Tag>
|
|
258
|
+
{props.dismissible && (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="hk-persistent-toast__dismiss"
|
|
262
|
+
aria-label={t("hikari::persistentToast.dismiss", "Dismiss")}
|
|
263
|
+
onClick={() => emit("dismiss")}
|
|
264
|
+
>
|
|
265
|
+
<X size={12} aria-hidden="true" />
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
268
|
+
{slots.detail && open.value && (
|
|
269
|
+
<Teleport to="body">
|
|
270
|
+
<div
|
|
271
|
+
ref={cardRef}
|
|
272
|
+
id={popupId}
|
|
273
|
+
class="hk-persistent-toast-card hii-dropdown-content"
|
|
274
|
+
role="group"
|
|
275
|
+
aria-label={props.detailLabel || undefined}
|
|
276
|
+
style={cardStyle.value}
|
|
277
|
+
onMouseenter={cancelClose}
|
|
278
|
+
onMouseleave={() => {
|
|
279
|
+
if (!pinned.value) scheduleClose();
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
{slots.detail()}
|
|
283
|
+
</div>
|
|
284
|
+
</Teleport>
|
|
285
|
+
)}
|
|
286
|
+
</span>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Chrome-bar chip row: chips + the "+N" overflow counter, one compact
|
|
2
|
+
// line. Token palette mirrors HkPersistentToast.scss so the pair reads
|
|
3
|
+
// as one family inside a title bar.
|
|
4
|
+
.hk-persistent-toast-group {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 4px;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.hk-persistent-toast-group__extra {
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
padding: 1px 6px;
|
|
16
|
+
border-radius: var(--radius-full, 999px);
|
|
17
|
+
background: rgb(var(--color-text) / 8%);
|
|
18
|
+
color: rgb(var(--color-text) / 72%);
|
|
19
|
+
font-size: var(--text-2xs, 0.6875rem);
|
|
20
|
+
line-height: 1.4;
|
|
21
|
+
font-variant-numeric: tabular-nums;
|
|
22
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkPersistentToast from "./HkPersistentToast";
|
|
5
|
+
import HkPersistentToastGroup from "./HkPersistentToastGroup";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HkPersistentToastGroup contract tests:
|
|
9
|
+
* - renders the first maxVisible children and collapses the rest into
|
|
10
|
+
* the "+N" counter
|
|
11
|
+
* - comment placeholders (v-if=false children) do not count
|
|
12
|
+
* - hidden children are unmounted, so their own DOM never reaches the
|
|
13
|
+
* document
|
|
14
|
+
*
|
|
15
|
+
* House style: raw createApp mounts torn down afterEach.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
19
|
+
|
|
20
|
+
function mountRow(count: number, maxVisible: number) {
|
|
21
|
+
const container = document.createElement("div");
|
|
22
|
+
document.body.appendChild(container);
|
|
23
|
+
const host = defineComponent({
|
|
24
|
+
setup() {
|
|
25
|
+
return () =>
|
|
26
|
+
h(
|
|
27
|
+
HkPersistentToastGroup,
|
|
28
|
+
{ maxVisible } as never,
|
|
29
|
+
{
|
|
30
|
+
default: () =>
|
|
31
|
+
Array.from({ length: count }, (_, i) =>
|
|
32
|
+
h(HkPersistentToast, { key: i, tone: "loading", label: `task ${i}` }),
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const app = createApp(host);
|
|
39
|
+
app.mount(container);
|
|
40
|
+
mounts.push({ app, container });
|
|
41
|
+
return { app, container };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
46
|
+
app.unmount();
|
|
47
|
+
container.remove();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function chips(c: HTMLElement) {
|
|
52
|
+
return c.querySelectorAll(".hk-persistent-toast");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("HkPersistentToastGroup", () => {
|
|
56
|
+
it("renders up to maxVisible chips and collapses the rest into +N", () => {
|
|
57
|
+
const { container } = mountRow(4, 1);
|
|
58
|
+
expect(chips(container).length).toBe(1);
|
|
59
|
+
expect(chips(container)[0]!.textContent).toContain("task 0");
|
|
60
|
+
const extra = container.querySelector<HTMLElement>(".hk-persistent-toast-group__extra")!;
|
|
61
|
+
expect(extra.textContent).toBe("+3");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders everything when count fits and skips the counter", () => {
|
|
65
|
+
const { container } = mountRow(2, 3);
|
|
66
|
+
expect(chips(container).length).toBe(2);
|
|
67
|
+
expect(container.querySelector(".hk-persistent-toast-group__extra")).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("unmounts hidden chips entirely (no stray DOM)", () => {
|
|
71
|
+
const { container } = mountRow(3, 1);
|
|
72
|
+
expect(document.body.textContent).not.toContain("task 1");
|
|
73
|
+
expect(document.body.textContent).not.toContain("task 2");
|
|
74
|
+
expect(chips(container).length).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders an empty group without noise", () => {
|
|
78
|
+
const { container } = mountRow(0, 1);
|
|
79
|
+
expect(chips(container).length).toBe(0);
|
|
80
|
+
expect(container.querySelector(".hk-persistent-toast-group__extra")).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Comment, defineComponent, type PropType, type SlotsType, type VNode } from "vue";
|
|
2
|
+
|
|
3
|
+
import "./HkPersistentToastGroup.scss";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HkPersistentToastGroup — a chrome-bar row of HkPersistentToast chips
|
|
7
|
+
* with a "+N" overflow: renders the first `maxVisible` children and
|
|
8
|
+
* collapses the rest into a passive counter. The wowsp title bar's
|
|
9
|
+
* loading chip ("newest label + +N") is the reference consumer.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <HPersistentToastGroup maxVisible={1}>
|
|
13
|
+
* {tasks.map((t) => (
|
|
14
|
+
* <HPersistentToast key={t.id} tone="loading" label={t.label} />
|
|
15
|
+
* ))}
|
|
16
|
+
* </HPersistentToastGroup>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Hidden children are UNMOUNTED, not merely hidden — their hover cards
|
|
20
|
+
* and timers are their own state and die with the vnode; a chip that
|
|
21
|
+
* re-enters the visible window starts fresh (a persistent toast has no
|
|
22
|
+
* meaningful state to preserve across an overflow eviction).
|
|
23
|
+
*/
|
|
24
|
+
export default defineComponent({
|
|
25
|
+
name: "HkPersistentToastGroup",
|
|
26
|
+
props: {
|
|
27
|
+
maxVisible: { type: Number as PropType<number>, default: 3 },
|
|
28
|
+
},
|
|
29
|
+
slots: Object as SlotsType<{
|
|
30
|
+
default?: () => unknown;
|
|
31
|
+
}>,
|
|
32
|
+
setup(props, { slots }) {
|
|
33
|
+
return () => {
|
|
34
|
+
const children = ((slots.default?.() ?? []) as VNode[]).filter((v) => v.type !== Comment);
|
|
35
|
+
const visible = children.slice(0, Math.max(0, props.maxVisible));
|
|
36
|
+
const extra = children.length - visible.length;
|
|
37
|
+
return (
|
|
38
|
+
<span class="hk-persistent-toast-group">
|
|
39
|
+
{visible}
|
|
40
|
+
{extra > 0 && <span class="hk-persistent-toast-group__extra">+{extra}</span>}
|
|
41
|
+
</span>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "قوة كلمة المرور",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "قوية",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "ضعيفة",
|
|
131
|
+
"hikari::persistentToast.dismiss": "تجاهل",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "اختر رمز الدولة",
|
|
132
133
|
"hikari::phoneInput.dialCode": "الرمز",
|
|
133
134
|
"hikari::phoneInput.noMatches": "لا توجد دولة مطابقة",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Passwortstärke",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Stark",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Schwach",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Ausblenden",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Ländercode wählen",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Code",
|
|
133
134
|
"hikari::phoneInput.noMatches": "Kein passendes Land",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Password strength",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Strong",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Weak",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Dismiss",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Choose country code",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Code",
|
|
133
134
|
"hikari::phoneInput.noMatches": "No matching country",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Fuerza de la contraseña",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Fuerte",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Débil",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Descartar",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Elegir prefijo país",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Código",
|
|
133
134
|
"hikari::phoneInput.noMatches": "Sin coincidencias de país",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Robustesse du mot de passe",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Fort",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Faible",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Ignorer",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Choisir l'indicatif pays",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Code",
|
|
133
134
|
"hikari::phoneInput.noMatches": "Aucun pays correspondant",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "パスワード強度",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "強い",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "弱い",
|
|
131
|
+
"hikari::persistentToast.dismiss": "閉じる",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "国コードを選択",
|
|
132
133
|
"hikari::phoneInput.dialCode": "コード",
|
|
133
134
|
"hikari::phoneInput.noMatches": "一致する国がありません",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "비밀번호 강도",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "강함",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "약함",
|
|
131
|
+
"hikari::persistentToast.dismiss": "닫기",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "국가 코드 선택",
|
|
132
133
|
"hikari::phoneInput.dialCode": "코드",
|
|
133
134
|
"hikari::phoneInput.noMatches": "일치하는 국가가 없습니다",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Força da senha",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Forte",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Fraca",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Dispensar",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Escolher código do país",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Código",
|
|
133
134
|
"hikari::phoneInput.noMatches": "Nenhum país correspondente",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "Надёжность пароля",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "Сильный",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "Слабый",
|
|
131
|
+
"hikari::persistentToast.dismiss": "Скрыть",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "Выберите код страны",
|
|
132
133
|
"hikari::phoneInput.dialCode": "Код",
|
|
133
134
|
"hikari::phoneInput.noMatches": "Подходящей страны не найдено",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "密码强度",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "强",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "弱",
|
|
131
|
+
"hikari::persistentToast.dismiss": "关闭",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "选择国家区号",
|
|
132
133
|
"hikari::phoneInput.dialCode": "区号",
|
|
133
134
|
"hikari::phoneInput.noMatches": "没有匹配的国家",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"hikari::passwordInput.strengthLabel": "密碼強度",
|
|
129
129
|
"hikari::passwordInput.strengthStrong": "強",
|
|
130
130
|
"hikari::passwordInput.strengthWeak": "弱",
|
|
131
|
+
"hikari::persistentToast.dismiss": "關閉",
|
|
131
132
|
"hikari::phoneInput.chooseCountry": "選擇國家區碼",
|
|
132
133
|
"hikari::phoneInput.dialCode": "區碼",
|
|
133
134
|
"hikari::phoneInput.noMatches": "沒有符合的國家",
|
package/src/index.ts
CHANGED
|
@@ -114,6 +114,8 @@ export {
|
|
|
114
114
|
acceptExtensions,
|
|
115
115
|
} from "./components/filePicker";
|
|
116
116
|
export { default as HToast } from "./components/HkToast";
|
|
117
|
+
export { default as HPersistentToast } from "./components/HkPersistentToast";
|
|
118
|
+
export { default as HPersistentToastGroup } from "./components/HkPersistentToastGroup";
|
|
117
119
|
export { default as HTooltip } from "./components/HkTooltip";
|
|
118
120
|
export { default as HTree } from "./components/HkTree";
|
|
119
121
|
export { default as HWindowedItem } from "./components/HkWindowedItem";
|
|
@@ -754,6 +756,8 @@ export { default as HkFileField } from "./components/HkFileField";
|
|
|
754
756
|
export { default as HkFileBrowserDialog } from "./components/HkFileBrowserDialog";
|
|
755
757
|
export { default as HkFilePickerField } from "./components/HkFilePickerField";
|
|
756
758
|
export { default as HkToast } from "./components/HkToast";
|
|
759
|
+
export { default as HkPersistentToast, type PersistentToastTone } from "./components/HkPersistentToast";
|
|
760
|
+
export { default as HkPersistentToastGroup } from "./components/HkPersistentToastGroup";
|
|
757
761
|
export { default as HkTooltip } from "./components/HkTooltip";
|
|
758
762
|
export { type HkTooltipSlotProps } from "./components/HkTooltip";
|
|
759
763
|
export { default as HkTree } from "./components/HkTree";
|