@icones/vanilla 0.0.1

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/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @icones/vanilla
2
+
3
+ 无框架依赖的 SVG 适配器,提供 **Light DOM Web Component**、普通 `<i>` 元素和手动 DOM API,复用核心加载器、缓存和渲染逻辑。
4
+
5
+ ## Web Component
6
+
7
+ ```html
8
+ <icones-icon name="tabler:star" size="24"></icones-icon>
9
+ <icones-icon name="tabler:heart" color="#7712f7"></icones-icon>
10
+
11
+ <script type="module">
12
+ import "@icones/vanilla/web-element"
13
+ </script>
14
+ ```
15
+
16
+ 未配置 `api` 时,明确的 `set:name` 会从 `https://<set>.icones.go-slim.dev/data/<name>.json` 加载。Vite 静态收集和本地 `sources` 优先;显式 scope 可用 `api: false` 关闭网络回退,或通过 `api` 覆盖为自建数据或 symbol 服务。
17
+
18
+ 只需导入一次,无需调用初始化函数。已有标签会自动升级,之后新增元素、属性更新、移除和重新插入都由浏览器生命周期处理。SVG 是标签的直接子元素,**不创建 Shadow DOM**,全局 CSS 可以直接选择 `icones-icon > svg`。
19
+
20
+ `web-element` 仅注册专用标签,不观察普通元素;下面的 `standard-element` 仅观察 `<i icon-name>`,不注册 Web Component。两个入口可单独导入,也可以同时导入,同一宿主不会重复渲染。原 `@icones/vanilla/register` 入口已更名为 `@icones/vanilla/web-element`。
21
+
22
+ 模块导入由 Vite 等构建工具解析,也可自行配置 import map。注册不改变应用已有的图标加载配置;配合 `@icones/vite`,HTML 入口中的 `name` / `alt-name` 字面量会静态提取(包括 template 内容)。动态名称和 JS 中的 HTML 字符串仍需要运行时 API 或本地 sources。
23
+
24
+ ## 普通 HTML 元素
25
+
26
+ ```html
27
+ <i icon-name="tabler:star" icon-size="24"></i>
28
+ <i icon-name="tabler:heart" icon-color="#7712f7" icon-defer="intersect"></i>
29
+
30
+ <script type="module">
31
+ import "@icones/vanilla/standard-element"
32
+ </script>
33
+ ```
34
+
35
+ 导入后自动扫描已有的 `i[icon-name]`,通过 MutationObserver 跟踪新增、移除和 `icon-*` 属性变化。SVG 直接追加到原有 i 内部,不替换宿主、删除原有子节点或监听器,也不创建 Shadow DOM。
36
+
37
+ 只识别 HTML `i` 标签的 `icon-name`;`<span icon-name>`、其他组件的 `icon` 属性以及 `data-icon*` / `data-defer` 都不会触发渲染。裸 `icon` 不是图标名别名。核心 SVG 的 `data-icon`、`data-state` 仍是输出元数据,不是声明式输入。
38
+
39
+ 普通元素中,给下表的 Web Component 属性加 `icon-` 前缀即可,例如 `name → icon-name`、`stroke-width → icon-stroke-width`、`defer → icon-defer`。三个 SVG 属性映射为 `decorative → icon-hidden`、`svg-role → icon-role`、`svg-class → icon-class`。
40
+
41
+ 用 `element.setAttribute("icon-name", "tabler:heart")` 或 `element.setAttribute("icon-show-alt", "true")` 更新;变化在 MutationObserver 回调中生效。原生 `class`、`style`、`aria-*` 和 `hidden` 留在 i 上,`icon-label`、`icon-hidden`、`icon-role`、`icon-class` 作用于 SVG。
42
+
43
+ 需要局部范围、scope 或自定义属性前缀时,使用主入口的显式 API,替代自动导入:
44
+
45
+ ```ts
46
+ import { bindIcons, createIconConfig } from "@icones/vanilla"
47
+
48
+ const icons = bindIcons({
49
+ root: document.querySelector("#toolbar")!,
50
+ scope: createIconConfig({ defaultSize: "lg" }),
51
+ })
52
+
53
+ await icons?.load() // 等待当前已激活的图标,不强制触发 icon-defer
54
+ // 页面/视图销毁时:
55
+ icons?.destroy()
56
+ ```
57
+
58
+ `root` 支持 Document、Element(包含自身)、DocumentFragment;不会自动穿透 Shadow DOM。省略时使用 document,没有 DOM 时为 no-op。默认观察变化;`observe: false` 时调用 `refresh()` 手动同步。重复初始化同一个 root 和 attrPrefix 返回现有句柄,不替换 scope;重叠 root 不会接管其他初始化器已持有的图标。原 `initIcons` API 不恢复。
59
+
60
+ 自动入口也导出 `standardElements` 句柄,可用于 `load()` 或 `destroy()`。`destroy()` 断开监听、取消延迟任务,并且只移除自身生成的 SVG;需要重新启动时调用 `bindIcons()`。
61
+
62
+ ## 自定义属性前缀
63
+
64
+ 使用主入口显式初始化,不要同时导入默认的 `standard-element` 自动入口:
65
+
66
+ ```ts
67
+ import { bindIcons } from "@icones/vanilla"
68
+
69
+ bindIcons({ attrPrefix: "ui-" })
70
+ ```
71
+
72
+ ```html
73
+ <i ui-name="tabler:deer" ui-size="24" ui-defer="intersect"></i>
74
+ ```
75
+
76
+ `attrPrefix` 默认是 `icon-`,包含末尾的连字符;接受以小写字母开头、由小写字母/数字及连字符分段组成的前缀,例如 `ui-`、`app-icon-`、`data-app-`。空值、不带结尾连字符、空白、大写字母或选择器字符会报错。
77
+
78
+ 所有属性统一使用配置的前缀,包括 name、size、width、height、color、fill、stroke-width、rotate、alt-name、show-alt、label、hidden、role、class 和 defer,不会回退到其他前缀。显式指定 `data-app-` 是自定义配置,并不是恢复 `data-icon*` 兼容别名。
79
+
80
+ 同一 root 可以分别初始化多个前缀,句柄与清理互不影响。一个宿主如同时声明多个已启用前缀的名称,由先初始化的句柄持有,建议每个元素只使用一个前缀。
81
+
82
+ Vite 静态提取不会推断运行时 bindIcons 参数,需同步配置:
83
+
84
+ ```ts
85
+ icones({ attrPrefixes: ["ui-"] })
86
+ ```
87
+
88
+ 该列表替换默认的 `["icon-"]`;同时使用两种前缀时配置 `["icon-", "ui-"]`。空列表关闭普通元素静态提取,不影响 Web Component。没有静态提取的动态名称仍需要本地 sources 或运行时 API。
89
+
90
+ ## Web Component 属性
91
+
92
+ | 属性 | 用法 |
93
+ | ------------------------ | ---------------------------------------------------------- |
94
+ | `name` | 图标名,例如 `tabler:star`;删除或设为空值会移除生成的 SVG |
95
+ | `size` | 像素数、`xs`–`xl` 预设或 CSS 长度,如 `24`、`lg`、`1.5rem` |
96
+ | `width` / `height` | 单独设置宽高;显式 size 优先 |
97
+ | `color` / `fill` | CSS 颜色 / SVG 填充 |
98
+ | `stroke-width` | 数字;`original` 保留原图描边 |
99
+ | `rotate` | 旋转四分之一圈的次数,`1` = 90° |
100
+ | `h-flip` / `v-flip` | 水平 / 垂直翻转 |
101
+ | `absolute-stroke-width` | 保持像素描边宽度 |
102
+ | `alt-name` / `show-alt` | 备用图标 / 是否显示备用图标 |
103
+ | `label` / `decorative` | 生成 SVG 的 aria-label / aria-hidden |
104
+ | `svg-role` / `svg-class` | 生成 SVG 的 role / class |
105
+ | `defer` | `intersect` 或 `domready`,延迟首次渲染 |
106
+
107
+ 布尔图标属性接受空值或 `"true"` 表示开启,`"false"` 表示关闭;移除恢复配置默认值。无效数字和布尔值被忽略。颜色和尺寸按 CSS 值传入。
108
+
109
+ 原生 `class`、`style`、`hidden`、`role`、`aria-*` 保留在宿主上,遵循 HTML 语义(例如 `hidden="false"` 仍是隐藏)。不要用 `hidden` 表示装饰性,请用 `decorative`。默认 SVG 是装饰性的;有意义的独立图标用 `label`,图标按钮则给外层 button 添加 `aria-label`:
110
+
111
+ ```html
112
+ <button aria-label="收藏">
113
+ <icones-icon name="tabler:star"></icones-icon>
114
+ </button>
115
+
116
+ <icones-icon name="tabler:heart" label="已收藏"></icones-icon>
117
+ ```
118
+
119
+ 用 `element.setAttribute("name", "tabler:heart")` 或 `element.setAttribute("show-alt", "true")` 更新。无需 MutationObserver 或全局 DOM 扫描。只管理自身生成的 SVG,不替换宿主、不删除其他子节点或监听器。移除元素会解除图标订阅并移除 SVG,共享缓存仍保留。
120
+
121
+ ## 样式
122
+
123
+ ```css
124
+ icones-icon {
125
+ color: var(--icon-color, currentColor);
126
+ }
127
+
128
+ icones-icon > svg {
129
+ vertical-align: -0.125em;
130
+ }
131
+
132
+ .toolbar icones-icon > svg {
133
+ width: 1.5rem;
134
+ height: 1.5rem;
135
+ }
136
+ ```
137
+
138
+ 也可以使用 `svg-class="product-icon"` 给 SVG 添加专用类。组件不注入样式表,也不改写宿主的 class 或内联样式。外部 SVG symbol 的内部 path 仍受跨文档边界限制,与是否使用 Shadow DOM 无关。
139
+
140
+ ## 配置和本地图标
141
+
142
+ 需要配置默认 scope 时,**用显式注册代替自动 web-element 导入**:
143
+
144
+ ```ts
145
+ import { defineIconElement, createIconConfig } from "@icones/vanilla"
146
+
147
+ const scope = createIconConfig({
148
+ api: false,
149
+ defaultSize: "lg",
150
+ sources: {
151
+ "app:check": [
152
+ [
153
+ "path",
154
+ {
155
+ fill: "none",
156
+ stroke: "currentColor",
157
+ strokeWidth: "2",
158
+ d: "m5 12 4 4L19 6",
159
+ },
160
+ ],
161
+ ],
162
+ },
163
+ })
164
+
165
+ defineIconElement({ scope })
166
+ ```
167
+
168
+ 然后使用 `<icones-icon name="app:check"></icones-icon>`。普通元素模式可将同一个 scope 传入 `bindIcons({ scope })`,再使用 `<i icon-name="app:check"></i>`。两种方式共享核心配置、缓存和加载器。本地元组 JSON 可经 `parseElementData` 校验后加入 sources。
169
+
170
+ 同一 CustomElementRegistry 只注册一次;重复调用安全,但不覆盖首次注册的默认 scope。浏览器不支持取消注册;修改组件实现后需刷新页面。如果同名标签已经被其他实现注册,会抛出明确错误。
171
+
172
+ 每个元素可以设置独立 scope(也支持注册前赋值):
173
+
174
+ ```ts
175
+ const icon = document.createElement("icones-icon")
176
+ icon.scope = createIconConfig({ defaultSize: "sm" }, scope)
177
+ icon.setAttribute("name", "app:check")
178
+ document.body.append(icon)
179
+
180
+ await icon.load() // 仅等待当前已激活的图标,不会强制触发 defer
181
+ icon.scope = undefined // 恢复本窗口首次注册时的默认 scope
182
+ icon.remove() // 自动清理
183
+ ```
184
+
185
+ `scope` 是 JS 属性,不是 HTML 字符串属性。显式注册可传入 `window` 用于 iframe 等独立环境。导入包或注册模块不要求 DOM;没有浏览器时注册不执行。服务端 SVG 输出请使用核心 `renderIcon`;Web Component 只在连接到浏览器文档后渲染。template 和脱离文档的元素在插入文档后才激活。
186
+
187
+ ## 延迟渲染
188
+
189
+ ```html
190
+ <icones-icon name="tabler:star" size="24" defer="intersect"></icones-icon>
191
+ <icones-icon name="tabler:heart" size="24" defer="domready"></icones-icon>
192
+
193
+ <i icon-name="tabler:star" icon-size="24" icon-defer="intersect"></i>
194
+ <i icon-name="tabler:heart" icon-size="24" icon-defer="domready"></i>
195
+ ```
196
+
197
+ - `intersect`:同一文档共享 IntersectionObserver,首次进入视口后停止观察;无此 API 时立即渲染。
198
+ - `domready`:文档仍在 loading 时等待 DOMContentLoaded;已 interactive / complete 时立即渲染。
199
+ - 未设置、空值或未知值:立即渲染。
200
+
201
+ 延迟期间不创建 SVG、不订阅或发起运行时图标请求;触发时读取最新属性和 scope。修改待激活元素的 defer(普通元素为 icon-defer)会切换等待条件,移除延迟属性会立即渲染。移除元素或删除名称属性会取消等待。首次激活后不再延迟,包括同一组件/初始化器生命周期中的移除后重新插入。
202
+
203
+ 避免布局跳动时,自行给空宿主预留尺寸:
204
+
205
+ ```css
206
+ icones-icon[defer="intersect"],
207
+ i[icon-defer="intersect"] {
208
+ display: inline-block;
209
+ width: 24px;
210
+ height: 24px;
211
+ }
212
+ ```
213
+
214
+ Vite 仍会静态提取延迟元素的名称;已内联的数据不会因此拆成懒加载模块。
215
+
216
+ ## 手动控制
217
+
218
+ ```ts
219
+ import { mountIcon, createIconConfig } from "@icones/vanilla"
220
+
221
+ const scope = createIconConfig({
222
+ api: { type: "symbol", baseUrl: "/icons" },
223
+ defaultSize: "lg",
224
+ })
225
+ const icon = mountIcon(
226
+ document.querySelector("#toolbar")!,
227
+ { name: "tabler:star", "aria-label": "收藏" },
228
+ scope
229
+ )
230
+
231
+ icon.update({ name: "tabler:heart", size: 32, color: "red" })
232
+ await icon.load()
233
+ icon.destroy()
234
+ ```
235
+
236
+ `createIcon(props, { scope, document })` 创建未挂载实例,返回 `element`、`update`、`load`、`destroy`。`mountIcon(target, props, scope?)` 同时挂载。`update` 接收完整 props;手动 API 仍需调用 `destroy` 解除订阅并移除 SVG。样式可使用 CSS 字符串或 CSS 属性名对象,额外 SVG 属性通过 `attributes` 传入,事件使用 `element.addEventListener`。
237
+
238
+ 开发:`bun run --cwd packages/vanilla play`。测试:`bun run --cwd packages/vanilla test`。完整 API 和 Vite 用法见[项目文档](../../README.md)。
@@ -0,0 +1,52 @@
1
+ import { n as htmlIconAttributes, t as createHtmlIconHost } from "./html-host-BIQv8_2n.js";
2
+ //#region src/element.ts
3
+ const tagName = "icones-icon";
4
+ const brand = Symbol.for("@icones/vanilla/element");
5
+ /** Register the light-DOM Web Component. Safe to import/call without a browser. */
6
+ function defineIconElement(options = {}) {
7
+ const view = options.window ?? (typeof window === "undefined" ? void 0 : window);
8
+ if (!view?.customElements || !view.HTMLElement) return;
9
+ const existing = view.customElements.get(tagName);
10
+ if (existing) {
11
+ if (!existing[brand]) throw new Error("icones-icon is already registered by another component.");
12
+ return existing;
13
+ }
14
+ class IconesElement extends view.HTMLElement {
15
+ static [brand] = true;
16
+ static observedAttributes = htmlIconAttributes("web");
17
+ #scope;
18
+ #upgraded = false;
19
+ #host = createHtmlIconHost(this, "web", () => this.#scope ?? options.scope, () => this.isConnected);
20
+ get scope() {
21
+ return this.#scope;
22
+ }
23
+ set scope(value) {
24
+ this.#scope = value;
25
+ this.#host.sync();
26
+ }
27
+ connectedCallback() {
28
+ if (!this.#upgraded) {
29
+ this.#upgraded = true;
30
+ if (Object.hasOwn(this, "scope")) {
31
+ const value = this.scope;
32
+ Reflect.deleteProperty(this, "scope");
33
+ this.#scope = value;
34
+ }
35
+ }
36
+ this.#host.sync();
37
+ }
38
+ disconnectedCallback() {
39
+ this.#host.disconnect();
40
+ }
41
+ attributeChangedCallback(_name, previous, next) {
42
+ if (previous !== next && this.#upgraded) this.#host.sync();
43
+ }
44
+ async load() {
45
+ await this.#host.load();
46
+ }
47
+ }
48
+ view.customElements.define(tagName, IconesElement);
49
+ return IconesElement;
50
+ }
51
+ //#endregion
52
+ export { defineIconElement as t };
@@ -0,0 +1,29 @@
1
+ import { IconScope } from "@icones/core";
2
+ //#region src/element.d.ts
3
+ interface IconElement extends HTMLElement {
4
+ /** Per-element configuration. Unset to use the registration's default scope. */
5
+ scope: IconScope | undefined;
6
+ /** Wait for the current SVG without activating a deferred element. */
7
+ load(): Promise<void>;
8
+ }
9
+ type IconElementConstructor = CustomElementConstructor & {
10
+ new (): IconElement;
11
+ };
12
+ type DefineIconElementOptions = {
13
+ /** Defaults to the current window. Useful for iframes and DOM tests. */
14
+ window?: {
15
+ HTMLElement: typeof HTMLElement;
16
+ customElements: CustomElementRegistry;
17
+ };
18
+ /** Initial default for this registry. Set element.scope for per-icon scopes. */
19
+ scope?: IconScope;
20
+ };
21
+ declare global {
22
+ interface HTMLElementTagNameMap {
23
+ "icones-icon": IconElement;
24
+ }
25
+ }
26
+ /** Register the light-DOM Web Component. Safe to import/call without a browser. */
27
+ declare function defineIconElement(options?: DefineIconElementOptions): IconElementConstructor | undefined;
28
+ //#endregion
29
+ export { defineIconElement as i, IconElement as n, IconElementConstructor as r, DefineIconElementOptions as t };
@@ -0,0 +1,248 @@
1
+ import { createIconController, defaultIconScope, iconStyleText, renderIcon } from "@icones/core";
2
+ //#region src/icon.ts
3
+ let nextId = 0;
4
+ /** Create a managed SVG. Call destroy when its owner is removed. */
5
+ function createIcon(initial, options = {}) {
6
+ const doc = options.document ?? globalThis.document;
7
+ if (!doc) throw new Error("createIcon requires a Document. Use renderIcon for server rendering.");
8
+ const element = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
9
+ const instanceId = `vanilla-${++nextId}`;
10
+ let props = initial;
11
+ let scope = options.scope ?? defaultIconScope();
12
+ let destroyed = false;
13
+ const controller = createIconController(props, scope);
14
+ const applied = /* @__PURE__ */ new Set();
15
+ function paint() {
16
+ if (destroyed) return;
17
+ const result = renderIcon(controller.getState(), props, scope.appearance, instanceId);
18
+ const attributes = {
19
+ ...props.attributes,
20
+ ...result.attributes,
21
+ class: props.class,
22
+ id: props.id
23
+ };
24
+ for (const key of applied) element.removeAttribute(key);
25
+ applied.clear();
26
+ for (const [key, value] of Object.entries(attributes)) {
27
+ if (value === void 0 || /^on/i.test(key) || key === "innerHTML") continue;
28
+ element.setAttribute(key, String(value));
29
+ applied.add(key);
30
+ }
31
+ element.style.cssText = iconStyleText(result.style);
32
+ if (typeof props.style === "string") element.style.cssText += ";" + props.style;
33
+ else for (const [key, value] of Object.entries(props.style ?? {})) if (value !== void 0) element.style.setProperty(key, String(value));
34
+ element.innerHTML = result.body;
35
+ }
36
+ const unsubscribe = controller.subscribe(paint);
37
+ paint();
38
+ return {
39
+ element,
40
+ update(next, nextScope = scope) {
41
+ if (destroyed) throw new Error("Icon has been destroyed.");
42
+ props = next;
43
+ scope = nextScope;
44
+ controller.update(props, scope);
45
+ },
46
+ async load() {
47
+ await controller.load();
48
+ paint();
49
+ },
50
+ destroy() {
51
+ destroyed = true;
52
+ unsubscribe();
53
+ controller.destroy();
54
+ element.remove();
55
+ }
56
+ };
57
+ }
58
+ function mountIcon(target, props, scope) {
59
+ const icon = createIcon(props, {
60
+ scope,
61
+ document: target.ownerDocument
62
+ });
63
+ target.append(icon.element);
64
+ return icon;
65
+ }
66
+ //#endregion
67
+ //#region src/html-host.ts
68
+ const props = [
69
+ "name",
70
+ "size",
71
+ "width",
72
+ "height",
73
+ "color",
74
+ "fill",
75
+ "stroke-width",
76
+ "absolute-stroke-width",
77
+ "rotate",
78
+ "h-flip",
79
+ "v-flip",
80
+ "alt-name",
81
+ "show-alt",
82
+ "label",
83
+ "hidden",
84
+ "role",
85
+ "class",
86
+ "defer"
87
+ ];
88
+ const webAttributes = {
89
+ hidden: "decorative",
90
+ role: "svg-role",
91
+ class: "svg-class"
92
+ };
93
+ /** Translate standardized attribute names for standard-element/web-component hosts. */
94
+ function attribute(mode, name, prefix) {
95
+ return mode === "standard" ? prefix + name : webAttributes[name] ?? name;
96
+ }
97
+ /** List all host attributes that can influence icon rendering. */
98
+ function htmlIconAttributes(mode, prefix = "icon-") {
99
+ return props.map((name) => attribute(mode, name, prefix));
100
+ }
101
+ /** Parse host attributes into typed icon props, with camelCase conversions pre-handled. */
102
+ function readProps(read) {
103
+ const text = (name) => read(name)?.trim() || void 0;
104
+ const number = (name) => {
105
+ const value = text(name);
106
+ return value !== void 0 && Number.isFinite(Number(value)) ? Number(value) : void 0;
107
+ };
108
+ const boolean = (name) => {
109
+ const value = read(name)?.trim();
110
+ return value === "" || value === "true" ? true : value === "false" ? false : void 0;
111
+ };
112
+ const dimension = (name) => number(name) ?? text(name);
113
+ return {
114
+ name: text("name"),
115
+ size: dimension("size"),
116
+ width: dimension("width"),
117
+ height: dimension("height"),
118
+ color: text("color"),
119
+ fill: text("fill"),
120
+ strokeWidth: number("stroke-width"),
121
+ style: text("stroke-width") === "original" ? { "--icones-stroke-width": "initial" } : void 0,
122
+ absoluteStrokeWidth: boolean("absolute-stroke-width"),
123
+ rotate: number("rotate"),
124
+ hFlip: boolean("h-flip"),
125
+ vFlip: boolean("v-flip"),
126
+ altName: text("alt-name"),
127
+ showAlt: boolean("show-alt"),
128
+ "aria-label": text("label"),
129
+ "aria-hidden": boolean("hidden"),
130
+ role: text("role"),
131
+ class: text("class")
132
+ };
133
+ }
134
+ const intersections = /* @__PURE__ */ new WeakMap();
135
+ /** Optional lazy-load helper for `defer="intersect"` strategy. */
136
+ function waitForIntersection(host, activate) {
137
+ const document = host.ownerDocument;
138
+ const Observer = document.defaultView?.IntersectionObserver;
139
+ if (!Observer) return;
140
+ let group = intersections.get(document);
141
+ if (!group) {
142
+ const callbacks = /* @__PURE__ */ new Map();
143
+ group = {
144
+ observer: new Observer((records) => {
145
+ for (const record of records) if (record.isIntersecting) callbacks.get(record.target)?.();
146
+ }),
147
+ callbacks
148
+ };
149
+ intersections.set(document, group);
150
+ }
151
+ const current = group;
152
+ current.callbacks.set(host, activate);
153
+ current.observer.observe(host);
154
+ return () => {
155
+ current.callbacks.delete(host);
156
+ current.observer.unobserve(host);
157
+ if (!current.callbacks.size) {
158
+ current.observer.disconnect();
159
+ intersections.delete(document);
160
+ }
161
+ };
162
+ }
163
+ /**
164
+ * Shared light-DOM rendering and lazy-load deferral used by both HTML APIs.
165
+ */
166
+ function createHtmlIconHost(host, mode, getScope, isActive, prefix = "icon-") {
167
+ const read = (name) => host.getAttribute(attribute(mode, name, prefix));
168
+ let icon;
169
+ let activated = false;
170
+ let waiting;
171
+ let cancelWait;
172
+ let signature;
173
+ let previousScope;
174
+ /** Cancel pending deferral callbacks and reset transition state. */
175
+ function stopWaiting() {
176
+ cancelWait?.();
177
+ cancelWait = void 0;
178
+ waiting = void 0;
179
+ }
180
+ /** Unmount any mounted icon and clear pending activation timers. */
181
+ function disconnect() {
182
+ stopWaiting();
183
+ icon?.destroy();
184
+ icon = void 0;
185
+ }
186
+ /** Move from deferred to active rendering when trigger condition is satisfied. */
187
+ function activate(trigger) {
188
+ if (waiting !== trigger) return;
189
+ stopWaiting();
190
+ if (!isActive() || !read("name")?.trim()) {
191
+ disconnect();
192
+ return;
193
+ }
194
+ if (read("defer")?.trim() !== trigger) {
195
+ sync();
196
+ return;
197
+ }
198
+ activated = true;
199
+ sync();
200
+ }
201
+ /** Reconcile host attributes into mounted SVG or mount one-time if absent. */
202
+ function sync() {
203
+ if (!isActive() || !read("name")?.trim()) {
204
+ disconnect();
205
+ return;
206
+ }
207
+ if (!activated) {
208
+ const defer = read("defer")?.trim();
209
+ if (waiting !== defer) stopWaiting();
210
+ if (defer === "domready" && host.ownerDocument.readyState === "loading") {
211
+ if (!cancelWait) {
212
+ const document = host.ownerDocument;
213
+ waiting = defer;
214
+ const ready = () => activate("domready");
215
+ document.addEventListener("DOMContentLoaded", ready, { once: true });
216
+ cancelWait = () => document.removeEventListener("DOMContentLoaded", ready);
217
+ }
218
+ return;
219
+ }
220
+ if (defer === "intersect") {
221
+ if (!cancelWait) {
222
+ waiting = defer;
223
+ cancelWait = waitForIntersection(host, () => activate("intersect"));
224
+ }
225
+ if (cancelWait) return;
226
+ }
227
+ stopWaiting();
228
+ activated = true;
229
+ }
230
+ const scope = getScope() ?? defaultIconScope();
231
+ const nextSignature = JSON.stringify(props.map(read));
232
+ if (icon) {
233
+ if (signature !== nextSignature || previousScope !== scope) icon.update(readProps(read), scope);
234
+ if (icon.element.parentNode !== host) host.append(icon.element);
235
+ } else icon = mountIcon(host, readProps(read), scope);
236
+ signature = nextSignature;
237
+ previousScope = scope;
238
+ }
239
+ return {
240
+ sync,
241
+ disconnect,
242
+ async load() {
243
+ await icon?.load();
244
+ }
245
+ };
246
+ }
247
+ //#endregion
248
+ export { mountIcon as i, htmlIconAttributes as n, createIcon as r, createHtmlIconHost as t };
@@ -0,0 +1,25 @@
1
+ import { i as defineIconElement, n as IconElement, r as IconElementConstructor, t as DefineIconElementOptions } from "./element-BTdxI8N4.js";
2
+ import { i as bindIcons, n as IconBindingOptions, r as IconBindingRoot, t as IconBinding } from "./standard-BmWh1yNR.js";
3
+ import { IconOptions, IconScope, createIconScope as createIconConfig } from "@icones/core";
4
+ export * from "@icones/core";
5
+ //#region src/icon.d.ts
6
+ type IconProps = IconOptions & {
7
+ class?: string;
8
+ id?: string;
9
+ style?: string | Record<string, string | number | undefined>;
10
+ attributes?: Record<string, string | number | boolean | undefined>;
11
+ };
12
+ type IconHandle = {
13
+ element: SVGSVGElement;
14
+ update(props: IconProps, scope?: IconScope): void;
15
+ load(): Promise<void>;
16
+ destroy(): void;
17
+ };
18
+ /** Create a managed SVG. Call destroy when its owner is removed. */
19
+ declare function createIcon(initial: IconProps, options?: {
20
+ scope?: IconScope;
21
+ document?: Document;
22
+ }): IconHandle;
23
+ declare function mountIcon(target: Element, props: IconProps, scope?: IconScope): IconHandle;
24
+ //#endregion
25
+ export { type DefineIconElementOptions, type IconBinding, type IconBindingOptions, type IconBindingRoot, type IconElement, type IconElementConstructor, type IconHandle, type IconProps, bindIcons, createIcon, createIconConfig, defineIconElement, mountIcon };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { i as mountIcon, r as createIcon } from "./html-host-BIQv8_2n.js";
2
+ import { t as defineIconElement } from "./element-B7yPwYLA.js";
3
+ import { t as bindIcons } from "./standard-Mk5BozI4.js";
4
+ import { createIconScope as createIconConfig } from "@icones/core";
5
+ export * from "@icones/core";
6
+ export { bindIcons, createIcon, createIconConfig, defineIconElement, mountIcon };
@@ -0,0 +1,23 @@
1
+ import { IconScope } from "@icones/core";
2
+ //#region src/standard.d.ts
3
+ type IconBindingRoot = Document | DocumentFragment | Element;
4
+ type IconBindingOptions = {
5
+ /** Defaults to document. Includes the root itself when it is an icon host. */
6
+ root?: IconBindingRoot;
7
+ /** Attribute prefix, including the trailing hyphen. Defaults to "icon-". */
8
+ attrPrefix?: string;
9
+ scope?: IconScope;
10
+ /** Defaults to true. With false, call refresh after DOM/attribute changes. */
11
+ observe?: boolean;
12
+ };
13
+ type IconBinding = {
14
+ refresh(): void;
15
+ /** Wait for mounted SVGs, without forcing deferred hosts to activate. */
16
+ load(): Promise<void>;
17
+ /** Stop observing and remove only this initializer's generated SVGs. */
18
+ destroy(): void;
19
+ };
20
+ /** Manage <i> hosts with the configured attribute prefix. A no-op during SSR. */
21
+ declare function bindIcons(options?: IconBindingOptions): IconBinding | undefined;
22
+ //#endregion
23
+ export { bindIcons as i, IconBindingOptions as n, IconBindingRoot as r, IconBinding as t };
@@ -0,0 +1,97 @@
1
+ import { n as htmlIconAttributes, t as createHtmlIconHost } from "./html-host-BIQv8_2n.js";
2
+ //#region src/standard.ts
3
+ const instances = /* @__PURE__ */ new WeakMap();
4
+ const owners = /* @__PURE__ */ new WeakMap();
5
+ function isHost(node, nameAttribute) {
6
+ const element = node;
7
+ return node.nodeType === 1 && element.namespaceURI === "http://www.w3.org/1999/xhtml" && element.localName === "i" && !!element.getAttribute(nameAttribute)?.trim();
8
+ }
9
+ /** Manage <i> hosts with the configured attribute prefix. A no-op during SSR. */
10
+ function bindIcons(options = {}) {
11
+ const prefix = options.attrPrefix ?? "icon-";
12
+ if (typeof prefix !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-$/.test(prefix)) throw new TypeError("attrPrefix must be a lowercase attribute prefix ending in \"-\", such as \"icon-\" or \"ui-\".");
13
+ const nameAttribute = prefix + "name";
14
+ const selector = "i[" + nameAttribute + "]";
15
+ const root = options.root ?? globalThis.document;
16
+ if (!root) return;
17
+ const registrations = instances.get(root) ?? /* @__PURE__ */ new Map();
18
+ const existing = registrations.get(prefix);
19
+ if (existing) return existing;
20
+ const Observer = (root.nodeType === 9 ? root : root.ownerDocument).defaultView?.MutationObserver ?? globalThis.MutationObserver;
21
+ if (options.observe !== false && !Observer) throw new Error("Standard elements require MutationObserver, or observe: false.");
22
+ const active = /* @__PURE__ */ new Map();
23
+ const cached = /* @__PURE__ */ new WeakMap();
24
+ let destroyed = false;
25
+ let observer;
26
+ function release(element, host) {
27
+ host.disconnect();
28
+ active.delete(element);
29
+ owners.delete(element);
30
+ }
31
+ function sync(element) {
32
+ if (destroyed) return;
33
+ const owner = owners.get(element);
34
+ if (owner && owner !== handle) return;
35
+ if (!root.contains(element) || !isHost(element, nameAttribute)) {
36
+ const host = active.get(element);
37
+ if (host) release(element, host);
38
+ return;
39
+ }
40
+ let host = cached.get(element);
41
+ if (!host) {
42
+ host = createHtmlIconHost(element, "standard", () => options.scope, () => !destroyed && root.contains(element) && owners.get(element) === handle, prefix);
43
+ cached.set(element, host);
44
+ }
45
+ owners.set(element, handle);
46
+ active.set(element, host);
47
+ host.sync();
48
+ }
49
+ function collect(node) {
50
+ if (isHost(node, nameAttribute)) sync(node);
51
+ if (node.nodeType !== 1 && node.nodeType !== 9 && node.nodeType !== 11) return;
52
+ if (node.nodeType === 1 && node.namespaceURI !== "http://www.w3.org/1999/xhtml") return;
53
+ for (const element of node.querySelectorAll(selector)) if (isHost(element, nameAttribute)) sync(element);
54
+ }
55
+ function prune() {
56
+ for (const [element, host] of active) if (!root.contains(element) || !isHost(element, nameAttribute)) release(element, host);
57
+ }
58
+ const handle = {
59
+ refresh() {
60
+ if (destroyed) return;
61
+ prune();
62
+ collect(root);
63
+ },
64
+ async load() {
65
+ handle.refresh();
66
+ await Promise.all([...active.values()].map((host) => host.load()));
67
+ },
68
+ destroy() {
69
+ if (destroyed) return;
70
+ destroyed = true;
71
+ observer?.disconnect();
72
+ for (const [element, host] of active) release(element, host);
73
+ registrations.delete(prefix);
74
+ if (!registrations.size) instances.delete(root);
75
+ }
76
+ };
77
+ if (options.observe !== false) {
78
+ observer = new Observer((records) => {
79
+ if (destroyed) return;
80
+ if (records.some((record) => record.removedNodes.length)) prune();
81
+ for (const record of records) if (record.type === "attributes") sync(record.target);
82
+ else for (const node of record.addedNodes) collect(node);
83
+ });
84
+ observer.observe(root, {
85
+ attributes: true,
86
+ attributeFilter: htmlIconAttributes("standard", prefix),
87
+ childList: true,
88
+ subtree: true
89
+ });
90
+ }
91
+ registrations.set(prefix, handle);
92
+ instances.set(root, registrations);
93
+ handle.refresh();
94
+ return handle;
95
+ }
96
+ //#endregion
97
+ export { bindIcons as t };
@@ -0,0 +1,5 @@
1
+ import { t as IconBinding } from "./standard-BmWh1yNR.js";
2
+ //#region src/standard-element.d.ts
3
+ declare const standardElements: IconBinding | undefined;
4
+ //#endregion
5
+ export { type IconBinding, standardElements };
@@ -0,0 +1,5 @@
1
+ import { t as bindIcons } from "./standard-Mk5BozI4.js";
2
+ //#region src/standard-element.ts
3
+ const standardElements = bindIcons();
4
+ //#endregion
5
+ export { standardElements };
@@ -0,0 +1,2 @@
1
+ import { n as IconElement } from "./element-BTdxI8N4.js";
2
+ export type { IconElement };
@@ -0,0 +1,4 @@
1
+ import { t as defineIconElement } from "./element-B7yPwYLA.js";
2
+ //#region src/web-element.ts
3
+ defineIconElement();
4
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@icones/vanilla",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "homepage": "https://icones.go-slim.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tint/icones.git",
9
+ "directory": "packages/vanilla"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/tint/icones/issues"
13
+ },
14
+ "sideEffects": [
15
+ "./dist/web-element.js",
16
+ "./dist/standard-element.js"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./web-element": {
25
+ "types": "./dist/web-element.d.ts",
26
+ "import": "./dist/web-element.js",
27
+ "default": "./dist/web-element.js"
28
+ },
29
+ "./standard-element": {
30
+ "types": "./dist/standard-element.d.ts",
31
+ "import": "./dist/standard-element.js",
32
+ "default": "./dist/standard-element.js"
33
+ }
34
+ },
35
+ "dependencies": {
36
+ "@icones/core": "^0.0.1"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }