@celestia-island/hikari 0.40.10 → 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/components/HkDrawer.tsx +1 -1
- 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/HkImageLightbox.tsx +1 -1
- package/src/components/HkModal.tsx +1 -1
- package/src/components/HkPopover.tsx +1 -1
- package/src/components/HkSelectPanel.tsx +1 -1
- package/src/components/func-icon-theme.contract.test.ts +44 -0
- package/src/components/window-close.contract.test.ts +5 -3
- package/src/components/window-close.scss +31 -4
- package/src/composables/iconRegistry.ts +100 -0
- package/src/index.ts +6 -1
package/package.json
CHANGED
|
@@ -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
|
|
@@ -563,7 +563,7 @@ export default defineComponent({
|
|
|
563
563
|
aria-label={t("hikari::modal.close", "Close")}
|
|
564
564
|
onClick={close}
|
|
565
565
|
>
|
|
566
|
-
<HIcon name="
|
|
566
|
+
<HIcon name="close" size={16} />
|
|
567
567
|
</HIconButton>
|
|
568
568
|
</div>
|
|
569
569
|
<div class="hk-select-sheet-body" ref={sheetBodyRef}>
|
|
@@ -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
|
+
});
|
|
@@ -28,14 +28,16 @@ const WINDOW_FILES = [
|
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
describe("unified window-close contract", () => {
|
|
31
|
-
it("renders every close control as HkIconButton + the
|
|
31
|
+
it("renders every close control as HkIconButton + the functional close key", () => {
|
|
32
32
|
for (const f of WINDOW_FILES) {
|
|
33
33
|
const src = readFileSync(join(here, f), "utf-8");
|
|
34
34
|
expect(src, `${f} must carry the shared grammar class`).toContain(
|
|
35
35
|
"hk-window-close",
|
|
36
36
|
);
|
|
37
|
-
|
|
38
|
-
|
|
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"',
|
|
39
41
|
);
|
|
40
42
|
expect(src, `${f} must close via HkIconButton`).toContain("HIconButton");
|
|
41
43
|
}
|
|
@@ -13,19 +13,46 @@
|
|
|
13
13
|
// gets the hover tint HkIconButton intentionally leaves to the Glow
|
|
14
14
|
// wrapper (window headers render no Glow). The `--hk-modal-close-hover-*`
|
|
15
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.
|
|
16
30
|
// ------
|
|
17
31
|
.hk-icon-button.hk-window-close {
|
|
18
32
|
user-select: none;
|
|
19
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);
|
|
20
41
|
|
|
21
42
|
&:hover {
|
|
22
43
|
background: var(
|
|
23
|
-
--hk-
|
|
24
|
-
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
|
+
)
|
|
25
49
|
);
|
|
26
50
|
color: var(
|
|
27
|
-
--hk-
|
|
28
|
-
var(
|
|
51
|
+
--hk-func-icon-hover-color,
|
|
52
|
+
var(
|
|
53
|
+
--hk-modal-close-hover-color,
|
|
54
|
+
var(--hi-color-text-primary, #333)
|
|
55
|
+
)
|
|
29
56
|
);
|
|
30
57
|
}
|
|
31
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
|
+
}
|
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
|
|