@celestia-island/hikari 0.40.9 → 0.40.11
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/DemoApp.tsx +4 -4
- package/src/components/HkAffixPicker.test.tsx +13 -5
- package/src/components/HkDrawer.scss +3 -24
- package/src/components/HkDrawer.test.tsx +19 -1
- package/src/components/HkDrawer.tsx +9 -15
- package/src/components/HkIcon.functional.test.ts +101 -0
- package/src/components/HkIcon.scss +15 -0
- package/src/components/HkIcon.tsx +27 -4
- package/src/components/HkIconButton.scss +7 -0
- package/src/components/HkIconButton.test.tsx +30 -0
- package/src/components/HkIconButton.tsx +11 -7
- package/src/components/HkImageLightbox.test.tsx +4 -0
- package/src/components/HkImageLightbox.tsx +4 -7
- package/src/components/HkLocalizedInput.test.tsx +11 -3
- package/src/components/HkMenu.scss +1 -1
- package/src/components/HkMenu.test.ts +10 -2
- package/src/components/HkModal.scss +4 -34
- package/src/components/HkModal.test.tsx +7 -0
- package/src/components/HkModal.tsx +13 -15
- package/src/components/HkPasswordInput.test.ts +2 -2
- package/src/components/HkPopover.scss +3 -38
- package/src/components/HkPopover.tsx +9 -15
- package/src/components/HkPopoverSheet.test.tsx +4 -0
- package/src/components/HkSelect.scss +28 -6
- package/src/components/HkSelectPanel.tsx +25 -3
- package/src/components/HkSelectSheet.test.tsx +31 -0
- package/src/components/func-icon-theme.contract.test.ts +44 -0
- package/src/components/window-close.contract.test.ts +54 -0
- package/src/components/window-close.scss +58 -0
- package/src/composables/iconRegistry.ts +100 -0
- package/src/data/dialCodes.test.ts +10 -3
- package/src/index.ts +6 -1
package/package.json
CHANGED
package/src/DemoApp.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// house rule: no SFCs in the tree). Render-only showcase of every exported
|
|
3
3
|
// component; state is local and throwaway.
|
|
4
4
|
import { computed, defineComponent, ref } from "vue";
|
|
5
|
-
import { PackageOpen } from "lucide-vue-next";
|
|
5
|
+
import { ChevronDown, ChevronUp, PackageOpen } from "lucide-vue-next";
|
|
6
6
|
import { HIKARI_FONT_MONO } from "./theme/fontContext";
|
|
7
7
|
import {
|
|
8
8
|
HButton, HIconButton, HTooltip, HBadge, HTag, HIcon, HSpinner,
|
|
@@ -214,7 +214,7 @@ export default defineComponent({
|
|
|
214
214
|
<div class="row">
|
|
215
215
|
<HButton variant="primary" loading>Loading</HButton>
|
|
216
216
|
<HButton variant="primary" disabled>Disabled</HButton>
|
|
217
|
-
<HButton variant="primary" shortcut="
|
|
217
|
+
<HButton variant="primary" shortcut="Ctrl+K">Shortcut</HButton>
|
|
218
218
|
</div>
|
|
219
219
|
</section>
|
|
220
220
|
|
|
@@ -337,7 +337,7 @@ export default defineComponent({
|
|
|
337
337
|
<div class="row">
|
|
338
338
|
<span style="align-self:center;color:var(--color-muted, gray)">custom trigger + checkbox rows:</span>
|
|
339
339
|
<span ref={panelAnchor} style="display:inline-flex">
|
|
340
|
-
<HButton onClick={() => (panel.value.open = !panel.value.open)}>Filter
|
|
340
|
+
<HButton onClick={() => (panel.value.open = !panel.value.open)}>Filter {panel.value.open ? <ChevronUp size={14} /> : <ChevronDown size={14} />}</HButton>
|
|
341
341
|
</span>
|
|
342
342
|
<HSelectPanel
|
|
343
343
|
open={panel.value.open}
|
|
@@ -393,7 +393,7 @@ export default defineComponent({
|
|
|
393
393
|
|
|
394
394
|
<section>
|
|
395
395
|
<h2>HKbd</h2>
|
|
396
|
-
<div class="row"><HKbd keys="
|
|
396
|
+
<div class="row"><HKbd keys="Ctrl" /><HKbd keys="Alt" /><HKbd keys="Shift" /><HKbd keys="K" /><span> = open command palette</span></div>
|
|
397
397
|
</section>
|
|
398
398
|
|
|
399
399
|
<section>
|
|
@@ -3,11 +3,19 @@ import { createApp, h, nextTick } from "vue";
|
|
|
3
3
|
|
|
4
4
|
import HkAffixPicker, { type HkAffixOption } from "./HkAffixPicker";
|
|
5
5
|
|
|
6
|
+
/** Regional-indicator flag pair for an ISO 3166-1 alpha-2 code, built
|
|
7
|
+
* from code points (same derivation as data/dialCodes flagEmoji) so the
|
|
8
|
+
* fixture source itself carries no literal emoji. */
|
|
9
|
+
const flagOf = (iso: string): string =>
|
|
10
|
+
String.fromCodePoint(
|
|
11
|
+
...Array.from(iso.toUpperCase(), (c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
12
|
+
);
|
|
13
|
+
|
|
6
14
|
const OPTIONS: HkAffixOption[] = [
|
|
7
|
-
{ key: "cn", label: "China", meta: "+86", flag: "
|
|
8
|
-
{ key: "cn-main", label: "中华人民共和国", meta: "+86", flag: "
|
|
9
|
-
{ key: "jp", label: "Japan", meta: "+81", flag: "
|
|
10
|
-
{ key: "us", label: "United States", meta: "+1", flag: "
|
|
15
|
+
{ key: "cn", label: "China", meta: "+86", flag: flagOf("cn"), keywords: "zhongguo 中国 0086" },
|
|
16
|
+
{ key: "cn-main", label: "中华人民共和国", meta: "+86", flag: flagOf("cn") },
|
|
17
|
+
{ key: "jp", label: "Japan", meta: "+81", flag: flagOf("jp") },
|
|
18
|
+
{ key: "us", label: "United States", meta: "+1", flag: flagOf("us") },
|
|
11
19
|
];
|
|
12
20
|
|
|
13
21
|
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
@@ -187,7 +195,7 @@ describe("HkAffixPicker", () => {
|
|
|
187
195
|
const labels = rows().map((r) => r.textContent);
|
|
188
196
|
// Both the China row (keyword substring match) and the renamed
|
|
189
197
|
// cn-main row (fuzzy subsequence) surface.
|
|
190
|
-
expect(labels).toContain("
|
|
198
|
+
expect(labels).toContain(`${flagOf("cn")}China+86`);
|
|
191
199
|
expect(labels.some((l) => l.includes("中华人民共和国"))).toBe(true);
|
|
192
200
|
});
|
|
193
201
|
|
|
@@ -124,32 +124,11 @@
|
|
|
124
124
|
color: var(--hi-color-text-primary, #333);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Legacy placement hook — the chrome lives on the shared .hk-icon-button
|
|
128
|
+
// / .hk-window-close rules (window-close.scss) since the unified-close
|
|
129
|
+
// wave; only the header-flex placement stays here.
|
|
127
130
|
.hk-drawer-close {
|
|
128
131
|
flex-shrink: 0;
|
|
129
|
-
width: 2rem;
|
|
130
|
-
height: 2rem;
|
|
131
|
-
display: inline-flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
justify-content: center;
|
|
134
|
-
padding: 0;
|
|
135
|
-
background: transparent;
|
|
136
|
-
border: none;
|
|
137
|
-
border-radius: var(--hi-radius-md, 0.5rem);
|
|
138
|
-
color: var(--hi-color-text-secondary, #666);
|
|
139
|
-
cursor: pointer;
|
|
140
|
-
transition:
|
|
141
|
-
background 0.15s ease,
|
|
142
|
-
color 0.15s ease;
|
|
143
|
-
|
|
144
|
-
&:hover {
|
|
145
|
-
background: var(--hi-secondary-bg, rgba(0, 0, 0, 0.06));
|
|
146
|
-
color: var(--hi-color-text-primary, #333);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
&:focus-visible {
|
|
150
|
-
outline: 2px solid var(--hi-color-primary, #58a6ff);
|
|
151
|
-
outline-offset: 2px;
|
|
152
|
-
}
|
|
153
132
|
}
|
|
154
133
|
|
|
155
134
|
/* Track host for the body's overlay scrollbar — owns the flex slot in
|
|
@@ -29,7 +29,7 @@ describe("HkDrawer back-guard (window-first back priority)", () => {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function mountDrawer(open: { value: boolean }, closable = true) {
|
|
32
|
+
function mountDrawer(open: { value: boolean }, closable = true, extra: Record<string, unknown> = {}) {
|
|
33
33
|
const container = document.createElement("div");
|
|
34
34
|
document.body.appendChild(container);
|
|
35
35
|
containers.push(container);
|
|
@@ -39,6 +39,7 @@ describe("HkDrawer back-guard (window-first back priority)", () => {
|
|
|
39
39
|
h(HkDrawer, {
|
|
40
40
|
modelValue: open.value,
|
|
41
41
|
closable,
|
|
42
|
+
...extra,
|
|
42
43
|
"onUpdate:modelValue": (v: boolean) => { open.value = v; },
|
|
43
44
|
}, { default: () => h("div", "content") });
|
|
44
45
|
},
|
|
@@ -62,6 +63,23 @@ describe("HkDrawer back-guard (window-first back priority)", () => {
|
|
|
62
63
|
app.unmount();
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
it("renders the unified icon-button close with the X glyph when titled", async () => {
|
|
67
|
+
const open = ref(true);
|
|
68
|
+
// The header (and thus the close) renders only with a title/header —
|
|
69
|
+
// pass one so the chrome exists to assert on.
|
|
70
|
+
const app = mountDrawer(open, true, { title: "Inspector" });
|
|
71
|
+
await settle();
|
|
72
|
+
|
|
73
|
+
const closeBtn = document.body.querySelector<HTMLButtonElement>(".hk-drawer-close")!;
|
|
74
|
+
expect(closeBtn).not.toBeNull();
|
|
75
|
+
expect(closeBtn.classList.contains("hk-window-close")).toBe(true);
|
|
76
|
+
// Unified close glyph: shared icon-button renders the registry X
|
|
77
|
+
// (default-slot branch, never the fallback placeholder circle).
|
|
78
|
+
expect(closeBtn.querySelector(".hk-icon")).not.toBeNull();
|
|
79
|
+
expect(closeBtn.querySelector("circle")).toBeNull();
|
|
80
|
+
app.unmount();
|
|
81
|
+
});
|
|
82
|
+
|
|
65
83
|
it("a non-closable drawer never owns history", async () => {
|
|
66
84
|
const open = ref(true);
|
|
67
85
|
const app = mountDrawer(open, false);
|
|
@@ -19,6 +19,9 @@ import { usePopupManager } from "../runtime/usePopupManager";
|
|
|
19
19
|
import { createBackGuard } from "../runtime/backStack";
|
|
20
20
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
21
21
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
22
|
+
import HIconButton from "./HkIconButton";
|
|
23
|
+
import HIcon from "./HkIcon";
|
|
24
|
+
import "./window-close.scss";
|
|
22
25
|
|
|
23
26
|
type DrawerSide = "left" | "right" | "top" | "bottom";
|
|
24
27
|
|
|
@@ -282,24 +285,15 @@ export default defineComponent({
|
|
|
282
285
|
<span class="hk-drawer-title">{props.title}</span>
|
|
283
286
|
)}
|
|
284
287
|
{props.closable ? (
|
|
285
|
-
<
|
|
286
|
-
class="hk-drawer-close"
|
|
288
|
+
<HIconButton
|
|
289
|
+
class="hk-window-close hk-drawer-close"
|
|
290
|
+
size={32}
|
|
291
|
+
variant="ghost"
|
|
287
292
|
aria-label={t("hikari::drawer.close", "Close")}
|
|
288
293
|
onClick={close}
|
|
289
294
|
>
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
fill="none"
|
|
293
|
-
stroke="currentColor"
|
|
294
|
-
stroke-width="2"
|
|
295
|
-
stroke-linecap="round"
|
|
296
|
-
width="16"
|
|
297
|
-
height="16"
|
|
298
|
-
>
|
|
299
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
300
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
301
|
-
</svg>
|
|
302
|
-
</button>
|
|
295
|
+
<HIcon name="close" size={16} />
|
|
296
|
+
</HIconButton>
|
|
303
297
|
) : null}
|
|
304
298
|
</div>
|
|
305
299
|
) : null}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, defineComponent, h } from "vue";
|
|
3
|
+
import { ChevronLeft, X } from "lucide-vue-next";
|
|
4
|
+
|
|
5
|
+
import HkIcon from "./HkIcon";
|
|
6
|
+
import {
|
|
7
|
+
functionalIconComponent,
|
|
8
|
+
functionalIconSvg,
|
|
9
|
+
hasFunctionalIconOverride,
|
|
10
|
+
registerFunctionalIconPack,
|
|
11
|
+
sanitizeSvg,
|
|
12
|
+
} from "../composables/iconRegistry";
|
|
13
|
+
|
|
14
|
+
const mounts: Array<ReturnType<typeof createApp>> = [];
|
|
15
|
+
|
|
16
|
+
function mountIcon(name: string): HTMLElement {
|
|
17
|
+
const container = document.createElement("div");
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
const app = createApp(
|
|
20
|
+
defineComponent({ setup: () => () => h(HkIcon, { name, size: 16 }) }),
|
|
21
|
+
);
|
|
22
|
+
mounts.push(app);
|
|
23
|
+
app.mount(container);
|
|
24
|
+
return container;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
for (const app of mounts.splice(0)) app.unmount();
|
|
29
|
+
registerFunctionalIconPack(null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("functional icon aliases and material packs", () => {
|
|
33
|
+
it("maps the semantic keys to the built-in family by default", () => {
|
|
34
|
+
// Identity with the lucide entries the aliases point at (lucide
|
|
35
|
+
// components carry no `.name` — identity is the honest check).
|
|
36
|
+
expect(functionalIconComponent("close")).toBe(X);
|
|
37
|
+
expect(functionalIconComponent("back")).toBe(ChevronLeft);
|
|
38
|
+
expect(hasFunctionalIconOverride("close")).toBe(false);
|
|
39
|
+
expect(functionalIconSvg("close")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the alias glyph through HkIcon without a pack", () => {
|
|
43
|
+
const c = mountIcon("close");
|
|
44
|
+
// lucide X renders as .lucide-x inside the .hk-icon wrapper.
|
|
45
|
+
expect(c.querySelector(".lucide-x")).not.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders a registered material-pack override instead of the alias", () => {
|
|
49
|
+
registerFunctionalIconPack({
|
|
50
|
+
close: '<svg viewBox="0 0 24 24" class="pack-close-mark"><path d="M4 4h16v16H4z"/></svg>',
|
|
51
|
+
});
|
|
52
|
+
expect(hasFunctionalIconOverride("close")).toBe(true);
|
|
53
|
+
const c = mountIcon("close");
|
|
54
|
+
expect(c.querySelector(".pack-close-mark")).not.toBeNull();
|
|
55
|
+
expect(c.querySelector(".lucide-x")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("leaves back untouched when the pack only carries close", () => {
|
|
59
|
+
registerFunctionalIconPack({
|
|
60
|
+
close: "<svg/>",
|
|
61
|
+
});
|
|
62
|
+
expect(hasFunctionalIconOverride("back")).toBe(false);
|
|
63
|
+
const c = mountIcon("back");
|
|
64
|
+
expect(c.querySelector(".lucide-chevron-left")).not.toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("sanitizes scripts and inline handlers out of pack markup", () => {
|
|
68
|
+
const dirty =
|
|
69
|
+
'<svg onload="alert(1)" onclick="x()"><script>alert(2)</script><path d="M4 4" onmouseover="y()"/></svg>';
|
|
70
|
+
const clean = sanitizeSvg(dirty);
|
|
71
|
+
expect(clean).not.toContain("script");
|
|
72
|
+
expect(clean).not.toContain("onload");
|
|
73
|
+
expect(clean).not.toContain("onclick");
|
|
74
|
+
expect(clean).not.toContain("onmouseover");
|
|
75
|
+
expect(clean).toContain("<path");
|
|
76
|
+
// sanitize applies on the functional render path too.
|
|
77
|
+
registerFunctionalIconPack({ close: dirty });
|
|
78
|
+
const c = mountIcon("close");
|
|
79
|
+
expect(c.querySelector("script")).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Round-2 review vectors (slash separators, unquoted schemes, SMIL).
|
|
83
|
+
it("neutralizes the round-2 evasion vectors", () => {
|
|
84
|
+
expect(sanitizeSvg("<svg/onload=alert(1)>")).not.toContain("onload");
|
|
85
|
+
expect(sanitizeSvg('<svg onload=alert(1)>')).not.toContain("onload");
|
|
86
|
+
expect(sanitizeSvg('<a href=javascript:alert(1)>t</a>')).not.toContain("javascript:");
|
|
87
|
+
expect(sanitizeSvg('<a href="javascript:alert(1)">t</a>')).not.toContain("javascript:");
|
|
88
|
+
expect(sanitizeSvg('<animate attributeName="href" values="javascript:alert(1)"/>')).not.toContain("animate");
|
|
89
|
+
expect(sanitizeSvg('<set attributeName="onmouseover" to="alert(1)"/>')).not.toContain("set");
|
|
90
|
+
expect(sanitizeSvg('<iframe src="javascript:alert(1)"></iframe>')).not.toContain("iframe");
|
|
91
|
+
expect(sanitizeSvg('<foreignObject><body onload="x()"></body></foreignObject>')).not.toContain("foreignObject");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("clearing the pack restores the built-in family", () => {
|
|
95
|
+
registerFunctionalIconPack({ close: '<svg class="pack"/><svg class="pack"/>' });
|
|
96
|
+
registerFunctionalIconPack(null);
|
|
97
|
+
expect(hasFunctionalIconOverride("close")).toBe(false);
|
|
98
|
+
const c = mountIcon("close");
|
|
99
|
+
expect(c.querySelector(".lucide-x")).not.toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -26,6 +26,21 @@
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// ------
|
|
30
|
+
// Functional icon-button glyph weight (theme-context driven)
|
|
31
|
+
// ------
|
|
32
|
+
// Inside the HkIconButton family — the HIGHLY FUNCTIONAL controls (window
|
|
33
|
+
// close/back, compact action buttons) — the THEME CONTEXT decides how the
|
|
34
|
+
// glyph is drawn, per user direction 2026-09-05: Blue Archive-styled themes
|
|
35
|
+
// keep the bold small default (stroke 2, no scaling) while thin-stroke
|
|
36
|
+
// themes (e.g. Endfield) publish --hk-func-icon-stroke-width / --hk-func-icon-scale
|
|
37
|
+
// from their own layer. A CSS rule here overrides lucide's inline
|
|
38
|
+
// stroke-width PRESENTATION ATTRIBUTE; scoping to .hk-icon-button keeps
|
|
39
|
+
// every plain HkIcon elsewhere untouched.
|
|
40
|
+
.hk-icon-button .hk-icon svg {
|
|
41
|
+
stroke-width: var(--hk-func-icon-stroke-width, 2);
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
// ------
|
|
30
45
|
// Icon Size Variants
|
|
31
46
|
// ------
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { defineComponent, type PropType } from "vue";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
functionalIconComponent,
|
|
4
|
+
functionalIconSvg,
|
|
5
|
+
iconByName,
|
|
6
|
+
} from "../composables/iconRegistry";
|
|
3
7
|
import "./HkIcon.scss";
|
|
4
8
|
|
|
9
|
+
/** Semantic functional keys resolved through the alias/pack pipeline. */
|
|
10
|
+
const FUNCTIONAL_KEYS = new Set(["close", "back"]);
|
|
11
|
+
|
|
5
12
|
export default defineComponent({
|
|
6
13
|
name: "HkIcon",
|
|
7
14
|
props: {
|
|
@@ -17,9 +24,25 @@ export default defineComponent({
|
|
|
17
24
|
props.color ? `hk-icon-${props.color}` : "",
|
|
18
25
|
];
|
|
19
26
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
27
|
+
// Functional keys ("close", "back") resolve through the material-pack
|
|
28
|
+
// pipeline FIRST (a host theme layer can swap the whole glyph family
|
|
29
|
+
// at runtime — sanitized raw SVG from the registered pack), then fall
|
|
30
|
+
// back to the alias's lucide component (close→X, back→ChevronLeft).
|
|
31
|
+
// Everything else resolves through the explicit lucide registry: a
|
|
32
|
+
// wildcard import of lucide-vue-next here defeated tree-shaking and
|
|
33
|
+
// shipped the whole ~1500-icon library in the shared bundle of every
|
|
34
|
+
// consumer.
|
|
35
|
+
if (FUNCTIONAL_KEYS.has(props.name)) {
|
|
36
|
+
const svg = functionalIconSvg(props.name);
|
|
37
|
+
if (svg) {
|
|
38
|
+
return (
|
|
39
|
+
<span class={cls} v-html={svg} />
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const FuncComp = functionalIconComponent(props.name) as any;
|
|
43
|
+
return <span class={cls}><FuncComp /></span>;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
const IconComp = iconByName(props.name) as any;
|
|
24
47
|
return <span class={cls}><IconComp /></span>;
|
|
25
48
|
};
|
|
@@ -151,6 +151,13 @@
|
|
|
151
151
|
height: var(--hi-icon-button-icon-size);
|
|
152
152
|
display: block;
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
// Theme-context glyph size (user direction 2026-09-05): BA themes keep
|
|
156
|
+
// scale 1; thin-stroke themes publish a >1 scale for their slightly
|
|
157
|
+
// larger functional glyphs. transform keeps the 32px hit box intact.
|
|
158
|
+
.hk-icon {
|
|
159
|
+
transform: scale(var(--hk-func-icon-scale, 1));
|
|
160
|
+
}
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
// Active state for icon
|
|
@@ -72,4 +72,34 @@ describe("HkIconButton", () => {
|
|
|
72
72
|
expect(btn.disabled).toBe(true);
|
|
73
73
|
expect(btn.getAttribute("aria-label")).toBe("sync");
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
// Unified window-close wave: children passed as the DEFAULT slot render
|
|
77
|
+
// as the icon (named `icon` slot still wins; no children at all keeps the
|
|
78
|
+
// placeholder circle). Without the default-slot branch, natural JSX usage
|
|
79
|
+
// `<HIconButton><HIcon name="X"/></HIconButton>` silently rendered the
|
|
80
|
+
// fallback circle instead of the consumer's glyph.
|
|
81
|
+
it("renders default-slot children as the icon", () => {
|
|
82
|
+
const c = mountComp(() =>
|
|
83
|
+
h(HkIconButton, { "aria-label": "close" }, { default: () => h("span", { class: "glyph-probe" }, "✕") }),
|
|
84
|
+
);
|
|
85
|
+
expect(c.querySelector(".glyph-probe")).not.toBeNull();
|
|
86
|
+
// The placeholder circle only renders when NEITHER slot is provided.
|
|
87
|
+
expect(c.querySelector("circle")).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("prefers the named icon slot over default children", () => {
|
|
91
|
+
const c = mountComp(() =>
|
|
92
|
+
h(HkIconButton, { "aria-label": "close" }, {
|
|
93
|
+
icon: () => h("span", { class: "named-probe" }),
|
|
94
|
+
default: () => h("span", { class: "default-probe" }),
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
expect(c.querySelector(".named-probe")).not.toBeNull();
|
|
98
|
+
expect(c.querySelector(".default-probe")).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("keeps the placeholder circle when no slot content is given", () => {
|
|
102
|
+
const c = mountComp(() => h(HkIconButton, { "aria-label": "probe" }));
|
|
103
|
+
expect(c.querySelector("circle")).not.toBeNull();
|
|
104
|
+
});
|
|
75
105
|
});
|
|
@@ -29,13 +29,17 @@ export default defineComponent({
|
|
|
29
29
|
{...attrs}
|
|
30
30
|
>
|
|
31
31
|
<span class="hk-icon-button-icon">
|
|
32
|
-
{slots.icon
|
|
33
|
-
slots.icon()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
{slots.icon
|
|
33
|
+
? slots.icon()
|
|
34
|
+
: slots.default
|
|
35
|
+
? // Natural children usage: <HIconButton><HIcon .../></HIconButton>
|
|
36
|
+
// renders the child as the icon (unified window-close wave).
|
|
37
|
+
slots.default()
|
|
38
|
+
: (
|
|
39
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
40
|
+
<circle cx="12" cy="12" r="10" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
39
43
|
</span>
|
|
40
44
|
</button>
|
|
41
45
|
);
|
|
@@ -61,6 +61,10 @@ describe("HkImageLightbox", () => {
|
|
|
61
61
|
|
|
62
62
|
const btn = s.closeButton();
|
|
63
63
|
expect(btn.getAttribute("aria-label")).toBe("Close");
|
|
64
|
+
// Unified close glyph: the shared icon-button renders the registry X
|
|
65
|
+
// (default-slot branch), never the fallback placeholder circle.
|
|
66
|
+
expect(btn.querySelector(".hk-icon")).not.toBeNull();
|
|
67
|
+
expect(btn.querySelector("circle")).toBeNull();
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
it("renders nothing while closed", () => {
|
|
@@ -3,8 +3,10 @@ import { defineComponent, onBeforeUnmount, watch } from "vue";
|
|
|
3
3
|
import { useI18n } from "../i18n/context";
|
|
4
4
|
import "./HkImageLightbox.scss";
|
|
5
5
|
import HIconButton from "./HkIconButton";
|
|
6
|
+
import HIcon from "./HkIcon";
|
|
6
7
|
import HImageViewer from "./HkImageViewer";
|
|
7
8
|
import HModal from "./HkModal";
|
|
9
|
+
import "./window-close.scss";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Immersive fullscreen viewer for a single image — the "special modal"
|
|
@@ -67,18 +69,13 @@ export default defineComponent({
|
|
|
67
69
|
<div class="hk-image-lightbox-stage">
|
|
68
70
|
<HImageViewer src={props.src} alt={props.alt} />
|
|
69
71
|
<HIconButton
|
|
70
|
-
class="hk-image-lightbox-close"
|
|
72
|
+
class="hk-window-close hk-image-lightbox-close"
|
|
71
73
|
size={32}
|
|
72
74
|
variant="ghost"
|
|
73
75
|
aria-label={t("hikari::imageLightbox.close", "Close")}
|
|
74
76
|
onClick={close}
|
|
75
77
|
>
|
|
76
|
-
|
|
77
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
78
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
79
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
80
|
-
</svg>
|
|
81
|
-
) }}
|
|
78
|
+
<HIcon name="close" size={16} />
|
|
82
79
|
</HIconButton>
|
|
83
80
|
</div>
|
|
84
81
|
</HModal>
|
|
@@ -3,6 +3,14 @@ import { createApp, h, nextTick } from "vue";
|
|
|
3
3
|
|
|
4
4
|
import { HkLocalizedInput } from "./HkLocalizedInput";
|
|
5
5
|
|
|
6
|
+
/** Regional-indicator flag pair for an ISO 3166-1 alpha-2 code, built
|
|
7
|
+
* from code points (same derivation as data/dialCodes flagEmoji) so the
|
|
8
|
+
* test source itself carries no literal emoji. */
|
|
9
|
+
const flagOf = (iso: string): string =>
|
|
10
|
+
String.fromCodePoint(
|
|
11
|
+
...Array.from(iso.toUpperCase(), (c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
12
|
+
);
|
|
13
|
+
|
|
6
14
|
const LOCALES = [
|
|
7
15
|
{ code: "en", label: "English" },
|
|
8
16
|
{ code: "zh-Hans", label: "简体中文" },
|
|
@@ -504,15 +512,15 @@ describe("HkLocalizedInput", () => {
|
|
|
504
512
|
sourceLang: "en",
|
|
505
513
|
translations: { en: "Plant overview", "zh-Hans": "工厂总览" },
|
|
506
514
|
localeOptions: [
|
|
507
|
-
{ code: "en", label: "English", flag: "
|
|
508
|
-
{ code: "zh-Hans", label: "简体中文", flag: "
|
|
515
|
+
{ code: "en", label: "English", flag: flagOf("gb") },
|
|
516
|
+
{ code: "zh-Hans", label: "简体中文", flag: flagOf("cn") },
|
|
509
517
|
],
|
|
510
518
|
}),
|
|
511
519
|
});
|
|
512
520
|
app.mount(container);
|
|
513
521
|
mounts.push({ app, container });
|
|
514
522
|
await openPicker(container);
|
|
515
|
-
expect(tag("简体中文")?.querySelector(".hk-affix-tag-flag")?.textContent).toBe("
|
|
523
|
+
expect(tag("简体中文")?.querySelector(".hk-affix-tag-flag")?.textContent).toBe(flagOf("cn"));
|
|
516
524
|
});
|
|
517
525
|
|
|
518
526
|
it("follows a sourceLang change without stealing focus", async () => {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
/* Leading check column (selectMode). The cell is reserved on EVERY row
|
|
37
37
|
* of a level while the column is on, so labels stay aligned; only
|
|
38
38
|
* checked === true shows the mark — an SVG check inside the same fixed
|
|
39
|
-
* cell grammar as leading icons (the old text
|
|
39
|
+
* cell grammar as leading icons (the old text glyph carried its own
|
|
40
40
|
* baseline rhythm and sat off-center against svg icons). */
|
|
41
41
|
.hk-menu-check {
|
|
42
42
|
@include mi.icon;
|
|
@@ -3,6 +3,14 @@ import { createApp, defineComponent, h, nextTick, ref, type ComponentPublicInsta
|
|
|
3
3
|
|
|
4
4
|
import HkMenu, { type HkMenuItem } from "./HkMenu";
|
|
5
5
|
|
|
6
|
+
/** Regional-indicator flag pair for an ISO 3166-1 alpha-2 code, built
|
|
7
|
+
* from code points (same derivation as data/dialCodes flagEmoji) so the
|
|
8
|
+
* fixture source itself carries no literal emoji. */
|
|
9
|
+
const flagOf = (iso: string): string =>
|
|
10
|
+
String.fromCodePoint(
|
|
11
|
+
...Array.from(iso.toUpperCase(), (c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
12
|
+
);
|
|
13
|
+
|
|
6
14
|
const mounts: ReturnType<typeof createApp>[] = [];
|
|
7
15
|
const containers: HTMLElement[] = [];
|
|
8
16
|
|
|
@@ -11,8 +19,8 @@ const items: HkMenuItem[] = [
|
|
|
11
19
|
key: "lang",
|
|
12
20
|
label: "Language",
|
|
13
21
|
children: [
|
|
14
|
-
{ key: "en", label: "English", flag: "
|
|
15
|
-
{ key: "zh", label: "中文", flag: "
|
|
22
|
+
{ key: "en", label: "English", flag: flagOf("gb") },
|
|
23
|
+
{ key: "zh", label: "中文", flag: flagOf("cn"), checked: true },
|
|
16
24
|
],
|
|
17
25
|
},
|
|
18
26
|
{ key: "logout", label: "Log out", danger: true },
|
|
@@ -179,42 +179,12 @@
|
|
|
179
179
|
-webkit-user-select: none;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// Legacy placement hook — the chrome (box, radius, colors, hover tint,
|
|
183
|
+
// selection) lives on the shared .hk-icon-button / .hk-window-close rules
|
|
184
|
+
// (window-close.scss) since the unified-close wave; only placement stays
|
|
185
|
+
// here so the header flex keeps the button pinned right.
|
|
182
186
|
.hk-modal-close {
|
|
183
187
|
flex-shrink: 0;
|
|
184
|
-
width: 2rem;
|
|
185
|
-
height: 2rem;
|
|
186
|
-
display: inline-flex;
|
|
187
|
-
align-items: center;
|
|
188
|
-
justify-content: center;
|
|
189
|
-
padding: 0;
|
|
190
|
-
background: transparent;
|
|
191
|
-
border: none;
|
|
192
|
-
border-radius: var(--hi-radius-md, 0.5rem);
|
|
193
|
-
color: var(--hk-modal-close-color, var(--hi-color-text-secondary, #666));
|
|
194
|
-
cursor: pointer;
|
|
195
|
-
transition:
|
|
196
|
-
background 0.15s ease,
|
|
197
|
-
color 0.15s ease;
|
|
198
|
-
|
|
199
|
-
&:hover {
|
|
200
|
-
background: var(
|
|
201
|
-
--hk-modal-close-hover-bg,
|
|
202
|
-
var(--hi-secondary-bg, rgba(0, 0, 0, 0.06))
|
|
203
|
-
);
|
|
204
|
-
color: var(
|
|
205
|
-
--hk-modal-close-hover-color,
|
|
206
|
-
var(--hi-color-text-primary, #333)
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
&:active {
|
|
211
|
-
transform: scale(0.92);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
&:focus-visible {
|
|
215
|
-
outline: 2px solid var(--hi-color-primary, #ffc0cb);
|
|
216
|
-
outline-offset: 2px;
|
|
217
|
-
}
|
|
218
188
|
}
|
|
219
189
|
|
|
220
190
|
// ------
|
|
@@ -139,6 +139,13 @@ describe("HkModal back-guard (window-first back priority)", () => {
|
|
|
139
139
|
await settle();
|
|
140
140
|
expect((window.history.state as Record<string, unknown>)?.__hkBack).toEqual(expect.any(String));
|
|
141
141
|
|
|
142
|
+
// Unified close chrome: the header close is the shared icon-button X
|
|
143
|
+
// (default-slot glyph, never the fallback placeholder circle).
|
|
144
|
+
const closeBtn = document.querySelector<HTMLButtonElement>(".hk-modal-close")!;
|
|
145
|
+
expect(closeBtn.classList.contains("hk-window-close")).toBe(true);
|
|
146
|
+
expect(closeBtn.querySelector(".hk-icon")).not.toBeNull();
|
|
147
|
+
expect(closeBtn.querySelector("circle")).toBeNull();
|
|
148
|
+
|
|
142
149
|
(document.querySelector(".hk-modal-close") as HTMLButtonElement).click();
|
|
143
150
|
await until(() => open.value === false);
|
|
144
151
|
// History rewound to the page base — a subsequent back leaves the page,
|
|
@@ -25,6 +25,9 @@ import { useSizeMorph } from "../composables/useSizeMorph";
|
|
|
25
25
|
import HButton from "./HkButton";
|
|
26
26
|
import HFab from "./HkFab";
|
|
27
27
|
import HSpinner from "./HkSpinner";
|
|
28
|
+
import HIconButton from "./HkIconButton";
|
|
29
|
+
import HIcon from "./HkIcon";
|
|
30
|
+
import "./window-close.scss";
|
|
28
31
|
|
|
29
32
|
export interface ModalAction {
|
|
30
33
|
label: string;
|
|
@@ -738,24 +741,19 @@ export default defineComponent({
|
|
|
738
741
|
{slots.header ? (props.title ?? "") : (props.title ?? "")}
|
|
739
742
|
</h2>
|
|
740
743
|
{props.closable && (
|
|
741
|
-
|
|
742
|
-
|
|
744
|
+
// Unified window-close affordance: the shared
|
|
745
|
+
// icon button + registry X glyph (see
|
|
746
|
+
// window-close.scss) — no per-component ✕ SVG.
|
|
747
|
+
// `hk-modal-close` stays as the placement hook.
|
|
748
|
+
<HIconButton
|
|
749
|
+
class="hk-window-close hk-modal-close"
|
|
750
|
+
size={32}
|
|
751
|
+
variant="ghost"
|
|
743
752
|
aria-label={t("hikari::modal.close", "Close")}
|
|
744
753
|
onClick={close}
|
|
745
754
|
>
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
fill="none"
|
|
749
|
-
stroke="currentColor"
|
|
750
|
-
stroke-width="2"
|
|
751
|
-
stroke-linecap="round"
|
|
752
|
-
width="16"
|
|
753
|
-
height="16"
|
|
754
|
-
>
|
|
755
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
756
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
757
|
-
</svg>
|
|
758
|
-
</button>
|
|
755
|
+
<HIcon name="close" size={16} />
|
|
756
|
+
</HIconButton>
|
|
759
757
|
)}
|
|
760
758
|
</div>
|
|
761
759
|
{slots.header && (
|
|
@@ -220,14 +220,14 @@ describe("HkPasswordInput variants and icons", () => {
|
|
|
220
220
|
"",
|
|
221
221
|
{ icon: "custom" },
|
|
222
222
|
{
|
|
223
|
-
icon: () => [h("span", { class: "custom-icon" }, "custom
|
|
223
|
+
icon: () => [h("span", { class: "custom-icon" }, "custom-mark")],
|
|
224
224
|
},
|
|
225
225
|
);
|
|
226
226
|
expect(
|
|
227
227
|
container.querySelector(".hk-pwd-lock")?.getAttribute("data-icon"),
|
|
228
228
|
).toBe("custom");
|
|
229
229
|
expect(container.querySelector(".custom-icon")?.textContent).toBe(
|
|
230
|
-
"custom
|
|
230
|
+
"custom-mark",
|
|
231
231
|
);
|
|
232
232
|
expect(container.querySelector(".hk-pwd-lock svg")).toBeNull();
|
|
233
233
|
});
|
|
@@ -187,45 +187,10 @@
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
.hk-popover-sheet-close {
|
|
190
|
-
|
|
191
|
-
//
|
|
190
|
+
// Legacy placement hook — the chrome lives on the shared .hk-icon-button
|
|
191
|
+
// / .hk-window-close rules (window-close.scss) since the unified-close
|
|
192
|
+
// wave. Only the right-edge pinning stays here.
|
|
192
193
|
margin-left: auto;
|
|
193
|
-
width: 2rem;
|
|
194
|
-
height: 2rem;
|
|
195
|
-
display: inline-flex;
|
|
196
|
-
align-items: center;
|
|
197
|
-
justify-content: center;
|
|
198
|
-
padding: 0;
|
|
199
|
-
background: transparent;
|
|
200
|
-
border: none;
|
|
201
|
-
border-radius: var(--hi-radius-md, 0.5rem);
|
|
202
|
-
color: var(--hk-modal-close-color, var(--hi-color-text-secondary, #666));
|
|
203
|
-
cursor: pointer;
|
|
204
|
-
user-select: none;
|
|
205
|
-
-webkit-user-select: none;
|
|
206
|
-
transition:
|
|
207
|
-
background 0.15s ease,
|
|
208
|
-
color 0.15s ease;
|
|
209
|
-
|
|
210
|
-
&:hover {
|
|
211
|
-
background: var(
|
|
212
|
-
--hk-modal-close-hover-bg,
|
|
213
|
-
var(--hi-secondary-bg, rgba(0, 0, 0, 0.06))
|
|
214
|
-
);
|
|
215
|
-
color: var(
|
|
216
|
-
--hk-modal-close-hover-color,
|
|
217
|
-
var(--hi-color-text-primary, #333)
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
&:active {
|
|
222
|
-
transform: scale(0.92);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
&:focus-visible {
|
|
226
|
-
outline: 2px solid var(--hi-color-primary, #ffc0cb);
|
|
227
|
-
outline-offset: 2px;
|
|
228
|
-
}
|
|
229
194
|
}
|
|
230
195
|
|
|
231
196
|
.hk-popover-sheet-enter-active {
|
|
@@ -19,6 +19,9 @@ import { useI18n } from "../i18n/context";
|
|
|
19
19
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
20
20
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
21
21
|
import { onceFrame } from "../runtime/animationBus";
|
|
22
|
+
import HIconButton from "./HkIconButton";
|
|
23
|
+
import HIcon from "./HkIcon";
|
|
24
|
+
import "./window-close.scss";
|
|
22
25
|
import "./HkPopover.scss";
|
|
23
26
|
|
|
24
27
|
export type PopupPlacement =
|
|
@@ -583,24 +586,15 @@ export default defineComponent({
|
|
|
583
586
|
{props.title && (
|
|
584
587
|
<div class="hk-popover-sheet-title">{props.title}</div>
|
|
585
588
|
)}
|
|
586
|
-
<
|
|
587
|
-
class="hk-popover-sheet-close"
|
|
589
|
+
<HIconButton
|
|
590
|
+
class="hk-window-close hk-popover-sheet-close"
|
|
591
|
+
size={32}
|
|
592
|
+
variant="ghost"
|
|
588
593
|
aria-label={t("hikari::modal.close", "Close")}
|
|
589
594
|
onClick={close}
|
|
590
595
|
>
|
|
591
|
-
<
|
|
592
|
-
|
|
593
|
-
fill="none"
|
|
594
|
-
stroke="currentColor"
|
|
595
|
-
stroke-width="2"
|
|
596
|
-
stroke-linecap="round"
|
|
597
|
-
width="16"
|
|
598
|
-
height="16"
|
|
599
|
-
>
|
|
600
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
601
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
602
|
-
</svg>
|
|
603
|
-
</button>
|
|
596
|
+
<HIcon name="close" size={16} />
|
|
597
|
+
</HIconButton>
|
|
604
598
|
</div>
|
|
605
599
|
)}
|
|
606
600
|
{slots.default?.()}
|
|
@@ -143,6 +143,10 @@ describe("HkPopover sheetOnMobile", () => {
|
|
|
143
143
|
const close = header.querySelector<HTMLButtonElement>(".hk-popover-sheet-close")!;
|
|
144
144
|
expect(close).toBeTruthy();
|
|
145
145
|
expect(close.getAttribute("aria-label")).toBeTruthy();
|
|
146
|
+
// The X glyph renders through the default-slot branch of HkIconButton
|
|
147
|
+
// (the fallback placeholder circle must never appear in a window close).
|
|
148
|
+
expect(close.querySelector(".hk-icon")).not.toBeNull();
|
|
149
|
+
expect(close.querySelector("circle")).toBeNull();
|
|
146
150
|
});
|
|
147
151
|
|
|
148
152
|
it("closes the sheet from the heading close button", async () => {
|
|
@@ -300,18 +300,40 @@
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
/* Sheet heading row — same unified grammar as the popover sheet header
|
|
304
|
+
* (title left, shared icon-button ✕ on the right edge, one vertically
|
|
305
|
+
* centered line). The select sheet previously had NO explicit close
|
|
306
|
+
* affordance at all: dismissal was grabber-tap/scrim only. */
|
|
307
|
+
.hk-select-sheet-header {
|
|
304
308
|
flex-shrink: 0;
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
padding: 0 var(--space-8) var(--space-8) 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.hk-select-sheet-close {
|
|
315
|
+
// Right-edge pinning with or without a title beside it; the chrome lives
|
|
316
|
+
// on the shared .hk-icon-button / .hk-window-close rules
|
|
317
|
+
// (window-close.scss) since the unified-close wave.
|
|
318
|
+
margin-left: auto;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.hk-select-sheet-title {
|
|
322
|
+
flex: 1;
|
|
323
|
+
min-width: 0;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
white-space: nowrap;
|
|
326
|
+
text-overflow: ellipsis;
|
|
305
327
|
/* Left inset tracks the sheet ENTRIES' text edge, not their card
|
|
306
328
|
* edge: the list pads option blocks in by --space-16 and rows carry
|
|
307
329
|
* roughly another --space-16 of leading padding inside, so the old
|
|
308
330
|
* single --space-16 left the title hanging left of every row it
|
|
309
331
|
* named instead of reading as the list's heading. Hosts tune it via
|
|
310
|
-
* --hk-sheet-title-inset
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
* --hk-sheet-title-inset. Size/weight match the popover sheet heading
|
|
333
|
+
* (--text-base, window chrome never text-selectable). The trailing
|
|
334
|
+
* inset doubles as the gap to the close button; the vertical rhythm
|
|
335
|
+
* moved to the header row. */
|
|
336
|
+
padding: 0 var(--space-16) 0 var(--hk-sheet-title-inset, calc(var(--space-16) * 2));
|
|
315
337
|
font-size: var(--text-base);
|
|
316
338
|
font-weight: 600;
|
|
317
339
|
letter-spacing: 0.02em;
|
|
@@ -17,6 +17,10 @@ import { createBackGuard } from "../runtime/backStack";
|
|
|
17
17
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
18
18
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
19
19
|
import { useSizeMorph } from "../composables/useSizeMorph";
|
|
20
|
+
import { useI18n } from "../i18n/context";
|
|
21
|
+
import HIconButton from "./HkIconButton";
|
|
22
|
+
import HIcon from "./HkIcon";
|
|
23
|
+
import "./window-close.scss";
|
|
20
24
|
import "./HkSelect.scss";
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -86,6 +90,7 @@ export default defineComponent({
|
|
|
86
90
|
},
|
|
87
91
|
setup(props, { emit, slots, expose }) {
|
|
88
92
|
const manager = usePopupManager();
|
|
93
|
+
const { t } = useI18n();
|
|
89
94
|
const handle = ref<PopupHandle | null>(null);
|
|
90
95
|
/** z of the last live registration — the leave transition keeps
|
|
91
96
|
* painting at this z after the registry entry is dropped, so a
|
|
@@ -541,9 +546,26 @@ export default defineComponent({
|
|
|
541
546
|
aria-hidden="true"
|
|
542
547
|
onClick={close}
|
|
543
548
|
/>
|
|
544
|
-
{
|
|
545
|
-
|
|
546
|
-
|
|
549
|
+
{/*
|
|
550
|
+
Unified sheet heading row — same grammar as the popover
|
|
551
|
+
sheet header (title left, shared icon-button ✕ on the
|
|
552
|
+
right edge). The select sheet previously had no explicit
|
|
553
|
+
close affordance at all (grabber/scrim only).
|
|
554
|
+
*/}
|
|
555
|
+
<div class="hk-select-sheet-header">
|
|
556
|
+
{props.title ? (
|
|
557
|
+
<div class="hk-select-sheet-title">{props.title}</div>
|
|
558
|
+
) : null}
|
|
559
|
+
<HIconButton
|
|
560
|
+
class="hk-window-close hk-select-sheet-close"
|
|
561
|
+
size={32}
|
|
562
|
+
variant="ghost"
|
|
563
|
+
aria-label={t("hikari::modal.close", "Close")}
|
|
564
|
+
onClick={close}
|
|
565
|
+
>
|
|
566
|
+
<HIcon name="close" size={16} />
|
|
567
|
+
</HIconButton>
|
|
568
|
+
</div>
|
|
547
569
|
<div class="hk-select-sheet-body" ref={sheetBodyRef}>
|
|
548
570
|
<div class="hk-select-sheet-list" ref={sheetListRef}>
|
|
549
571
|
<div class="hk-select-sheet-content" ref={sheetContentRef}>
|
|
@@ -103,6 +103,37 @@ describe("HkSelect mobile sheet", () => {
|
|
|
103
103
|
expect(container.querySelector<HTMLElement>(".hk-select-trigger")!.dataset.state).toBe("closed");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
// Unified window-close wave: the select sheet previously had no explicit
|
|
107
|
+
// close affordance (grabber/scrim only). It now carries the same heading
|
|
108
|
+
// row as the popover sheet — title left, shared icon-button ✕ right.
|
|
109
|
+
it("closes the sheet via the heading close button", async () => {
|
|
110
|
+
setViewport(375);
|
|
111
|
+
const { container } = mountSelect();
|
|
112
|
+
await nextTick();
|
|
113
|
+
|
|
114
|
+
container.querySelector<HTMLButtonElement>(".hk-select-trigger")!.click();
|
|
115
|
+
await nextTick();
|
|
116
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-select-sheet-panel")!;
|
|
117
|
+
const header = panel.querySelector<HTMLElement>(".hk-select-sheet-header")!;
|
|
118
|
+
expect(header).toBeTruthy();
|
|
119
|
+
expect(header.querySelector(".hk-select-sheet-title")!.textContent).toBe("Channel");
|
|
120
|
+
const close = header.querySelector<HTMLButtonElement>(".hk-select-sheet-close")!;
|
|
121
|
+
expect(close).toBeTruthy();
|
|
122
|
+
expect(close.getAttribute("aria-label")).toBeTruthy();
|
|
123
|
+
// attrs merging through HkIconButton keeps BOTH the shared icon-button
|
|
124
|
+
// chrome classes and the consumer's close hooks on the same element.
|
|
125
|
+
expect(close.classList.contains("hk-icon-button")).toBe(true);
|
|
126
|
+
expect(close.classList.contains("hk-icon-button-ghost")).toBe(true);
|
|
127
|
+
expect(close.classList.contains("hk-window-close")).toBe(true);
|
|
128
|
+
// The X glyph actually renders (default-slot branch, not the fallback
|
|
129
|
+
// placeholder circle).
|
|
130
|
+
expect(close.querySelector(".hk-icon")).not.toBeNull();
|
|
131
|
+
expect(close.querySelector("circle")).toBeNull();
|
|
132
|
+
close.click();
|
|
133
|
+
await nextTick();
|
|
134
|
+
expect(container.querySelector<HTMLElement>(".hk-select-trigger")!.dataset.state).toBe("closed");
|
|
135
|
+
});
|
|
136
|
+
|
|
106
137
|
it("keeps the anchored popout on desktop widths", async () => {
|
|
107
138
|
setViewport(1200);
|
|
108
139
|
const { container } = mountSelect();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract for the theme-context-driven functional icon rendering
|
|
3
|
+
* (user direction 2026-09-05: the close/back functional controls are drawn
|
|
4
|
+
* BY THE THEME CONTEXT — Blue Archive themes keep the bold small default,
|
|
5
|
+
* thin-stroke themes like Endfield publish their own weight/size/motion).
|
|
6
|
+
*
|
|
7
|
+
* The mechanism is a pure custom-property family: a theme layer publishes
|
|
8
|
+
* --hk-func-icon-* on any root and every HkIconButton glyph + window-close
|
|
9
|
+
* hover picks it up. No JS plumbing, no per-theme component forks.
|
|
10
|
+
*
|
|
11
|
+
* Pinned here so the consumption points cannot silently disappear (the
|
|
12
|
+
* same "looks themed but never shipped" failure class as the sheet-dock
|
|
13
|
+
* contracts).
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const read = (f: string): string => readFileSync(join(here, f), "utf-8");
|
|
22
|
+
|
|
23
|
+
describe("functional icon theme-context contract", () => {
|
|
24
|
+
it("draws icon-button glyphs with the theme stroke width", () => {
|
|
25
|
+
const css = read("HkIcon.scss");
|
|
26
|
+
// Scoped to the functional family: plain HkIcons elsewhere must keep
|
|
27
|
+
// the lucide default weight.
|
|
28
|
+
expect(css).toContain(".hk-icon-button .hk-icon svg");
|
|
29
|
+
expect(css).toContain("var(--hk-func-icon-stroke-width, 2)");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("scales icon-button glyphs with the theme size factor", () => {
|
|
33
|
+
const css = read("HkIconButton.scss");
|
|
34
|
+
expect(css).toContain("var(--hk-func-icon-scale, 1)");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("expresses the window-close hover motion through the var family", () => {
|
|
38
|
+
const css = read("window-close.scss");
|
|
39
|
+
expect(css).toContain("var(--hk-func-icon-hover-duration, 0.15s)");
|
|
40
|
+
expect(css).toContain("var(--hk-func-icon-hover-ease, ease)");
|
|
41
|
+
expect(css).toContain("--hk-func-icon-hover-bg,");
|
|
42
|
+
expect(css).toContain("--hk-func-icon-hover-color,");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract for the unified window-close affordance (user directive:
|
|
3
|
+
* unify on the shared icon button + one close glyph, not per-component ✕
|
|
4
|
+
* SVG copies).
|
|
5
|
+
*
|
|
6
|
+
* Every window chrome close control — modal header, drawer header,
|
|
7
|
+
* popover/select mobile-sheet heading row — must be an HkIconButton
|
|
8
|
+
* carrying the registry's X glyph and the shared `hk-window-close`
|
|
9
|
+
* grammar class (window-close.scss). No window component keeps its own
|
|
10
|
+
* inline ✕ line-SVG any more: one glyph, one chrome, many windows.
|
|
11
|
+
*
|
|
12
|
+
* Pinned here so a refactor cannot silently re-sprout per-component close
|
|
13
|
+
* SVG copies — the same "looks unified but drifted" failure class the
|
|
14
|
+
* sheet-dock contracts guard against.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const WINDOW_FILES = [
|
|
23
|
+
"HkModal.tsx",
|
|
24
|
+
"HkPopover.tsx",
|
|
25
|
+
"HkDrawer.tsx",
|
|
26
|
+
"HkSelectPanel.tsx",
|
|
27
|
+
"HkImageLightbox.tsx",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
describe("unified window-close contract", () => {
|
|
31
|
+
it("renders every close control as HkIconButton + the functional close key", () => {
|
|
32
|
+
for (const f of WINDOW_FILES) {
|
|
33
|
+
const src = readFileSync(join(here, f), "utf-8");
|
|
34
|
+
expect(src, `${f} must carry the shared grammar class`).toContain(
|
|
35
|
+
"hk-window-close",
|
|
36
|
+
);
|
|
37
|
+
// Semantic key: the theme context's material pack can swap the glyph
|
|
38
|
+
// family at runtime (iconRegistry resolves close → X by default).
|
|
39
|
+
expect(src, `${f} must render the functional close key`).toContain(
|
|
40
|
+
'HIcon name="close"',
|
|
41
|
+
);
|
|
42
|
+
expect(src, `${f} must close via HkIconButton`).toContain("HIconButton");
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("keeps no per-component inline ✕ SVG duplicates", () => {
|
|
47
|
+
for (const f of WINDOW_FILES) {
|
|
48
|
+
const src = readFileSync(join(here, f), "utf-8");
|
|
49
|
+
expect(src, `${f} must not re-inline the ✕ line SVG`).not.toContain(
|
|
50
|
+
'x1="18"',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ------
|
|
2
|
+
// Unified window-close affordance grammar.
|
|
3
|
+
//
|
|
4
|
+
// Every window chrome close control — modal header, drawer header,
|
|
5
|
+
// popover/select mobile-sheet heading row — renders the SAME thing: an
|
|
6
|
+
// HkIconButton (ghost, 32px box) carrying the registry's X glyph
|
|
7
|
+
// (HkIcon name="X"). No component keeps its own inline ✕ SVG any more;
|
|
8
|
+
// the glyph comes from the shared lucide registry and the chrome comes
|
|
9
|
+
// from the icon-button, so there is exactly one source for each.
|
|
10
|
+
//
|
|
11
|
+
// This partial only carries the window-chrome grammar ON TOP of the icon
|
|
12
|
+
// button: a close control never participates in text selection, and it
|
|
13
|
+
// gets the hover tint HkIconButton intentionally leaves to the Glow
|
|
14
|
+
// wrapper (window headers render no Glow). The `--hk-modal-close-hover-*`
|
|
15
|
+
// custom properties stay the shared host tunables for that tint.
|
|
16
|
+
//
|
|
17
|
+
// THE THEME CONTEXT DECIDES THE MOTION (user direction 2026-09-05): the
|
|
18
|
+
// hover choreography is expressed entirely through the --hk-func-icon-*
|
|
19
|
+
// custom properties, so a theme layer restyles every functional control
|
|
20
|
+
// without touching this file:
|
|
21
|
+
// --hk-func-icon-stroke-width glyph weight (consumed in HkIcon.scss)
|
|
22
|
+
// --hk-func-icon-scale glyph size scale (HkIconButton.scss)
|
|
23
|
+
// --hk-func-icon-hover-duration hover transition length
|
|
24
|
+
// --hk-func-icon-hover-ease hover transition curve
|
|
25
|
+
// --hk-func-icon-hover-bg hover background (transparent = snap)
|
|
26
|
+
// --hk-func-icon-hover-color hover glyph color
|
|
27
|
+
// Defaults below are the Blue Archive grammar: soft tint fade. A thin-
|
|
28
|
+
// stroke theme (Endfield) publishes a transparent background, a snap
|
|
29
|
+
// duration and an accent glyph color instead.
|
|
30
|
+
// ------
|
|
31
|
+
.hk-icon-button.hk-window-close {
|
|
32
|
+
user-select: none;
|
|
33
|
+
-webkit-user-select: none;
|
|
34
|
+
transition:
|
|
35
|
+
background
|
|
36
|
+
var(--hk-func-icon-hover-duration, 0.15s)
|
|
37
|
+
var(--hk-func-icon-hover-ease, ease),
|
|
38
|
+
color
|
|
39
|
+
var(--hk-func-icon-hover-duration, 0.15s)
|
|
40
|
+
var(--hk-func-icon-hover-ease, ease);
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
background: var(
|
|
44
|
+
--hk-func-icon-hover-bg,
|
|
45
|
+
var(
|
|
46
|
+
--hk-modal-close-hover-bg,
|
|
47
|
+
var(--hi-secondary-bg, rgba(0, 0, 0, 0.06))
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
color: var(
|
|
51
|
+
--hk-func-icon-hover-color,
|
|
52
|
+
var(
|
|
53
|
+
--hk-modal-close-hover-color,
|
|
54
|
+
var(--hi-color-text-primary, #333)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -125,3 +125,103 @@ export function iconByName(name: string | undefined | null): Component {
|
|
|
125
125
|
}
|
|
126
126
|
return Info;
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// ------
|
|
130
|
+
// Functional icon aliases + host-provided material packs (材质包).
|
|
131
|
+
//
|
|
132
|
+
// Window chrome renders SEMANTIC names ("close", "back") instead of
|
|
133
|
+
// literal lucide names, so a host theme layer can swap the whole glyph
|
|
134
|
+
// family at runtime — user direction 2026-09-05. Resolution order in
|
|
135
|
+
// functionalIconSvg():
|
|
136
|
+
// 1. host material-pack override for the semantic key (raw SVG string,
|
|
137
|
+
// sanitized at render time),
|
|
138
|
+
// 2. the semantic alias's default lucide component,
|
|
139
|
+
// 3. `Info` (never reached for the built-in keys).
|
|
140
|
+
// `registerFunctionalIconPack(null)` clears the pack and restores the
|
|
141
|
+
// built-in family. Packs ride the module singleton: applications register
|
|
142
|
+
// once at theme-apply time, exactly like the CSS-var publication.
|
|
143
|
+
// ------
|
|
144
|
+
|
|
145
|
+
/** Semantic key → default lucide component for the built-in family. */
|
|
146
|
+
const FUNCTIONAL_ALIASES: Record<string, Component> = {
|
|
147
|
+
close: X,
|
|
148
|
+
back: ChevronLeft,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let functionalPack: Record<string, string> | null = null;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Install (or clear with `null`) the host's material pack: semantic key →
|
|
155
|
+
* raw SVG markup. The SVG string is sanitized when rendered (script blocks
|
|
156
|
+
* and event-handler attributes stripped); callers own upload validation.
|
|
157
|
+
*/
|
|
158
|
+
export function registerFunctionalIconPack(
|
|
159
|
+
pack: Record<string, string> | null,
|
|
160
|
+
): void {
|
|
161
|
+
functionalPack = pack && Object.keys(pack).length > 0 ? pack : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** True when a material pack carries an override for `key`. */
|
|
165
|
+
export function hasFunctionalIconOverride(key: string): boolean {
|
|
166
|
+
return !!functionalPack && !!functionalPack[key];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Hardened sanitizer for host-provided SVG markup (render path is v-html).
|
|
171
|
+
*
|
|
172
|
+
* Deterministic + synchronous by design — the render path cannot await a
|
|
173
|
+
* lazy DOMPurify chunk. Hardened against the classic evasion classes
|
|
174
|
+
* (round-2 review vectors): slash-separated event attributes
|
|
175
|
+
* (`<svg/onload=…>`), unquoted `javascript:` URLs, dangerous element
|
|
176
|
+
* families (script/iframe/object/embed/foreignObject/animate/set/base/
|
|
177
|
+
* meta/form) and SMIL attribute-mutation primitives. NOT an HTML/SVG
|
|
178
|
+
* allowlist parser: material packs are user-local configuration today —
|
|
179
|
+
* if they ever become shareable files, switch to the DOMPurify allowlist
|
|
180
|
+
* (the dependency already ships for HkMarkdownRenderer) before doing so.
|
|
181
|
+
*/
|
|
182
|
+
const SVG_DENIED_TAGS =
|
|
183
|
+
/<(\/?)(script|iframe|object|embed|foreignObject|animate|set|base|meta|form)\b[^>]*>/gi;
|
|
184
|
+
const SVG_SCRIPT_BLOCKS = /<script\b[\s\S]*?<\/script\s*>/gi;
|
|
185
|
+
// Value groups deliberately STOP at their own closing quote — a greedy
|
|
186
|
+
// any-char alternation would swallow the rest of the tag and let later
|
|
187
|
+
// event attributes survive.
|
|
188
|
+
const SVG_EVENT_ATTRS =
|
|
189
|
+
/[\s/]on[a-z-]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
190
|
+
const SVG_SCHEME_ATTRS =
|
|
191
|
+
/(\s(?:xlink:)?(?:href|src)\s*=\s*)("[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
192
|
+
const SVG_DANGEROUS_SCHEME = /^\s*(?:javascript|vbscript|data:text\/html)/i;
|
|
193
|
+
|
|
194
|
+
export function sanitizeSvg(svg: string): string {
|
|
195
|
+
// 1. Script blocks and whole dangerous element families are removed
|
|
196
|
+
// (SMIL mutation primitives like <animate>/<set> included).
|
|
197
|
+
// 2. Event handler attributes — slash separators included — are renamed
|
|
198
|
+
// to an inert attribute; the handler can never fire again.
|
|
199
|
+
// 3. URL-carrying attributes with script-ish schemes are dropped whole.
|
|
200
|
+
let out = svg
|
|
201
|
+
.replace(SVG_SCRIPT_BLOCKS, "")
|
|
202
|
+
.replace(SVG_DENIED_TAGS, "<$1nothing>");
|
|
203
|
+
out = out.replace(SVG_EVENT_ATTRS, (_m, value: string) => ` data-stripped=${value}`);
|
|
204
|
+
out = out.replace(SVG_SCHEME_ATTRS, (match, _prefix: string, value: string) =>
|
|
205
|
+
SVG_DANGEROUS_SCHEME.test(value.replace(/^["']/, "").replace(/["']$/, ""))
|
|
206
|
+
? ""
|
|
207
|
+
: match,
|
|
208
|
+
);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Render payload for a functional icon key: a raw-SVG string when the
|
|
214
|
+
* material pack overrides it, `null` when the caller should fall back to
|
|
215
|
+
* the alias's lucide component.
|
|
216
|
+
*/
|
|
217
|
+
export function functionalIconSvg(key: string): string | null {
|
|
218
|
+
if (functionalPack && functionalPack[key]) {
|
|
219
|
+
return sanitizeSvg(functionalPack[key]);
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Default lucide component for a functional key (alias lookup). */
|
|
225
|
+
export function functionalIconComponent(key: string): Component {
|
|
226
|
+
return FUNCTIONAL_ALIASES[key] ?? Info;
|
|
227
|
+
}
|
|
@@ -32,9 +32,16 @@ describe("dialCodes catalog", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it("derives flag emoji from ISO codes", () => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// Expected regional-indicator pair built from code points so the
|
|
36
|
+
// test source carries no literal emoji while still pinning the
|
|
37
|
+
// exact runtime output byte-for-byte.
|
|
38
|
+
const expectedFlag = (iso: string): string =>
|
|
39
|
+
String.fromCodePoint(
|
|
40
|
+
...Array.from(iso.toUpperCase(), (c) => 0x1f1e6 + c.charCodeAt(0) - 65),
|
|
41
|
+
);
|
|
42
|
+
expect(flagEmoji("cn")).toBe(expectedFlag("cn"));
|
|
43
|
+
expect(flagEmoji("us")).toBe(expectedFlag("us"));
|
|
44
|
+
expect(flagEmoji("zz")).toBe(expectedFlag("zz")); // any two letters map
|
|
38
45
|
expect(flagEmoji("")).toBe("");
|
|
39
46
|
});
|
|
40
47
|
});
|
package/src/index.ts
CHANGED
|
@@ -269,7 +269,12 @@ export type { HealthProbeBody, HealthProbeResult } from "./utils/healthProbe";
|
|
|
269
269
|
|
|
270
270
|
export { highlight, useHighlight } from "./composables/useHighlight";
|
|
271
271
|
export { LANGUAGE_LOADERS } from "./composables/highlightLanguages";
|
|
272
|
-
export {
|
|
272
|
+
export {
|
|
273
|
+
iconByName,
|
|
274
|
+
registerFunctionalIconPack,
|
|
275
|
+
functionalIconSvg,
|
|
276
|
+
sanitizeSvg,
|
|
277
|
+
} from "./composables/iconRegistry";
|
|
273
278
|
export { useMessaging, registerTransport, registerNativeBridge } from "./composables/messaging";
|
|
274
279
|
export type { MessagePayload, MessageSeverity, MessageTransport, NotifyOptions, TransportName } from "./composables/messaging";
|
|
275
280
|
|