@icones/core 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.
Files changed (57) hide show
  1. package/README.md +72 -0
  2. package/RUNTIME.md +81 -0
  3. package/UTILITIES.md +30 -0
  4. package/dist/catalog.d.ts +17 -0
  5. package/dist/catalog.js +115 -0
  6. package/dist/controller.d.ts +27 -0
  7. package/dist/controller.js +156 -0
  8. package/dist/data-DtdeiZ5o.d.ts +18 -0
  9. package/dist/data-N59s4mFY.js +189 -0
  10. package/dist/data.d.ts +4 -0
  11. package/dist/data.js +36 -0
  12. package/dist/element-types-C9jOyda4.d.ts +9 -0
  13. package/dist/element-types.d.ts +2 -0
  14. package/dist/element-types.js +0 -0
  15. package/dist/elements-DLSCanEZ.d.ts +16 -0
  16. package/dist/elements.d.ts +3 -0
  17. package/dist/elements.js +29 -0
  18. package/dist/icon-data-HGvJGwoB.d.ts +42 -0
  19. package/dist/icon-data.d.ts +2 -0
  20. package/dist/icon-data.js +95 -0
  21. package/dist/index.d.ts +17 -0
  22. package/dist/index.js +15 -0
  23. package/dist/loaders-DxxA-L6t.d.ts +58 -0
  24. package/dist/loaders.d.ts +2 -0
  25. package/dist/loaders.js +112 -0
  26. package/dist/manifest.d.ts +9 -0
  27. package/dist/manifest.js +87 -0
  28. package/dist/presentation.d.ts +94 -0
  29. package/dist/presentation.js +189 -0
  30. package/dist/registry.d.ts +25 -0
  31. package/dist/registry.js +66 -0
  32. package/dist/resource-types.d.ts +68 -0
  33. package/dist/resource-types.js +0 -0
  34. package/dist/runtime.d.ts +7 -0
  35. package/dist/runtime.js +12 -0
  36. package/dist/set-options-C2BMGh-r.d.ts +11 -0
  37. package/dist/set-options.d.ts +2 -0
  38. package/dist/set-options.js +23 -0
  39. package/dist/sizes-CMxyEIpX.d.ts +10 -0
  40. package/dist/sizes.d.ts +2 -0
  41. package/dist/sizes.js +0 -0
  42. package/dist/slug.d.ts +5 -0
  43. package/dist/slug.js +7 -0
  44. package/dist/sources-CXcG9DjB.d.ts +11 -0
  45. package/dist/store.d.ts +36 -0
  46. package/dist/store.js +265 -0
  47. package/dist/svg-data.d.ts +2 -0
  48. package/dist/svg-data.js +2 -0
  49. package/dist/svg.d.ts +19 -0
  50. package/dist/svg.js +49 -0
  51. package/dist/symbol.d.ts +7 -0
  52. package/dist/symbol.js +22 -0
  53. package/dist/types.d.ts +30 -0
  54. package/dist/types.js +0 -0
  55. package/dist/view-box.d.ts +6 -0
  56. package/dist/view-box.js +9 -0
  57. package/package.json +133 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @icones/core
2
+
3
+ 框架无关的图标基础层,为 CSR/SSR 组件、Vite 和资源工具提供数据处理、渲染、缓存及加载器。浏览器入口不依赖 React 或 Node 文件系统。
4
+
5
+ CSR/SSR 的 JS 和类型声明均不依赖 `@iconify/*`。基础 SVG 变换、本地集合解析由自有轻量工具处理;上游格式转换留在 dev/build。未配置 `api` / `sources` / `loader` 时不请求网络,缺失图标保持 missing/fallback;子配置仍会继承应用显式提供的加载器。
6
+
7
+ 静态 JSON、函数加载器和可选第三方接入见 [运行时数据接入](./RUNTIME.md)。
8
+
9
+ 源码按 `data`、`svg`、`options`、`runtime`、`resources` 分组;职责、依赖方向和后续归属评估见 [源码结构](./src/README.md)。这是内部目录调整,现有公开入口与构建产物路径保持不变。
10
+
11
+ `IconName`、`IconSetName` 和 `IconNamesBySet` 从轻量的 `@icones/names` 重新导出给适配器。Core 不依赖完整图标数据包。`IconName<"tabler">` 可限定到单个集合;`Name` 还允许动态名称和自定义 sources。运行时不加载这份名称清单或图标数据。
12
+
13
+ `data` / `altData` 只接收单个图标对象或元素元组数组,不接收名称字符串或整个 `IconSet`。集合通过 `sources: { tabler: collection }` 注册,再用 `name="tabler:star"` / `altName="tabler:heart"` 选择。每组同时传入名称和单图标数据仍会输出 `console.error`,并优先使用数据。
14
+
15
+ ```ts
16
+ import { createIconStore } from "@icones/core"
17
+
18
+ const store = createIconStore({
19
+ api: { type: "fetch", baseUrl: "/icons" },
20
+ concurrency: 6,
21
+ })
22
+ await store.load("tabler:search")
23
+ console.log(store.getState("tabler:search"))
24
+ ```
25
+
26
+ Core 的根入口负责 SSR/CSR 共用的配置、渲染、加载状态、缓存和请求作用域,以及 tuple 序列化、SVG 字符串处理和基础数据校验。原 utils 实现已合并,原有 Core 导出保持兼容;工具接口见 [基础工具](./UTILITIES.md)。
27
+
28
+ 目录筛选、manifest 校验与序列化只通过 `@icones/core/catalog`、`@icones/core/manifest` 和 `@icones/core/resource-types` 子入口提供,不从运行时根入口导出。MCP、Vite、生成器和网站可以直接使用这些无状态入口,不加载组件注册表、store 或 controller。Core 不反向依赖 Vite、文件仓库或 XML 解析器。
29
+
30
+ 网站目录客户端、分页和样式展示策略归 `app/src/features/catalog/`;开发/构建期文件能力归 [Vite server 子入口](../vite/SERVER.md),MCP 独立管理只读文件访问;导入、转换与资源生成归 [Vite tooling](../vite/TOOLING.md) 和 `scripts/icon-builder/`。`core/runtime` 只保留构建结果在 CSR/SSR 中需要的注册与解析接口,不包含插件钩子。
31
+
32
+ 官网的本地静态文件托管与 React Router 回退位于 `app/server/`,不属于 Core。维护者仍可从工作区根目录运行 `bun run start`;生产部署只需发布静态产物。
33
+
34
+ `maxEntries`(默认 512)限制 store 中的 LRU 状态缓存,包括同步来源、symbol 引用、错误状态和 `initialData`。订阅中、请求中的条目不会被淘汰;刚读取的同步快照也会暂时保留,以便组件完成订阅,因此活跃图标较多时可能暂时超过上限,取消订阅或请求结束后会重新收缩。此限制不裁剪调用方提供的 `sources` 数据。
35
+
36
+ `initialData` 超过容量时只保留最后写入的条目。SSR/hydration 需要保留全部预加载数据时,请让服务端和客户端的 `maxEntries` 至少覆盖本次页面需要的图标数量。
37
+
38
+ 完整用法与数据协议见 [项目文档](../../README.md)。
39
+
40
+ ## 内置 viewBox
41
+
42
+ `iconViewBoxes` 和 `getIconViewBox(name)` 提供三个预设:普通图标默认 `0 0 24 24`,`flag:*-circle` / `flag:*-square` 使用 `0 0 512 512`,其余 `flag:*` 使用 `0 0 640 480`。命名 Flag 的内联、Fetch 和 Symbol 使用同一规则;宽高仍由 `size` 控制,4x3 旗帜按比例居中,不需要额外的缩放组。
43
+
44
+ 直接传入 Data 不根据名称猜测旗帜类型,沿用普通数据渲染。已有数据中的显式画布(例如 Phosphor 的 256×256)及自定义 Symbol API 的 `viewBox` 仍然保留。远程 Flag symbols 保留原始坐标;其他远程 symbols 继续归一化为 24×24。升级时需同步生成并发布 `icons/flag/symbols/`,避免新运行时配合旧的归一化 Flag 文件。
45
+
46
+ 适配器可以通过 `createIconScope` 管理每应用/每请求配置,通过 `createIconController` 订阅和加载图标,再用纯函数 `renderIcon` 生成 SVG 属性、body 和样式。创建 controller 本身不启动请求;`subscribe` 或 `load` 才触发加载。服务端使用独立 scope;`renderIcon` 的 `instanceId` 必须在同一文档中唯一,hydration 前后保持一致。
47
+
48
+ ## 按图标集配置
49
+
50
+ ```ts
51
+ import { createIconScope } from "@icones/core"
52
+
53
+ const scope = createIconScope({
54
+ defaultSize: { tabler: "lg", default: "md" },
55
+ sizeValues: { tabler: { lg: 28 }, default: { xl: 32 } },
56
+ strokeWidth: { tabler: 2, default: 1.5 },
57
+ absoluteStrokeWidth: { tabler: true, default: false },
58
+ api: {
59
+ tabler: { type: "fetch", baseUrl: "https://icons.example.com/icons" },
60
+ flag: { type: "symbol", baseUrl: "/icons" },
61
+ default: false,
62
+ },
63
+ })
64
+ ```
65
+
66
+ `defaultSize`、`sizeValues`、`strokeWidth`、`absoluteStrokeWidth` 和 `api` 都支持共享值或 set/default 映射。`sources` 和 `store` 保持原有格式。所有适配器共享同一解析规则:单个图标的显式属性 → 当前 set → `default` → 内置默认值。`default` 是兜底键,不是分类或样式。
67
+
68
+ 尺寸映射值支持命名预设、数值和 CSS 长度。`sizeValues` 支持原有的扁平字典(如 `{ lg: 28 }`),也支持每个 set 下放一个预设字典;解析某个尺寸时,按该 set 的预设 → `default` 中的同名预设 → 内置预设回退。内置值为 xs: 12、sm: 16、md: 20、lg: 24、xl: 28。
69
+
70
+ 子级映射按 set 合并并继承父级兜底。子级单值会替换该选项的整个映射;`sizeValues` 则始终按预设名称合并,扁平字典只更新共享预设,不移除继承的 set 专属预设。仅修改外观不会新建图标 store。匿名 Data 使用兜底值,通过 sources 注册的图标按名称中的 set 匹配,备用图标按当前显示的来源匹配。
71
+
72
+ `api` 每个 set 可使用 URL 字符串、fetch 选项、symbol 选项、加载函数或 `false`,也可直接传给 `createIconStore({ api })`。覆盖某个 set 会替换该项完整 API 配置,不合并请求参数;共享 API 值(包括 `false`)会替换整个继承映射。`type`、`baseUrl`、`url`、`fetch`、`requestInit`、`transform`、`viewBox` 是 API 选项保留键。`false` 仅禁用回退 API,不禁用本地来源、构建期图标或显式 per-icon loader。示例地址需替换为自己的服务;symbol 使用同源 URL,SSR fetch 使用绝对 URL。
package/RUNTIME.md ADDED
@@ -0,0 +1,81 @@
1
+ # 运行时数据接入
2
+
3
+ 组件链路为适配器 → Core,不包含 `@iconify/*`,包括生成的类型声明。数据/SVG 工具已经并入 Core;目录和 manifest 仍隔离在独立子入口。`IconData` 是自有 SVG body/尺寸结构,`ElementData` 是元素元组;本地 `IconSet` 只提供名称、公共尺寸和简单别名继承,不处理第三方目录元数据。数据必须来自可信来源,这些接口不负责净化 SVG。
4
+
5
+ 各框架适配器的默认加载器使用 Icones 自有静态服务:`<set>:<name>` 映射为 `https://<set>.icones.go-slim.dev/data/<name>.json`。本地 sources、显式 per-icon loader 和 Vite 构建产物仍然优先;用户显式设置的 `api` 会替换默认服务,`api: false` 可以完全关闭网络回退。
6
+
7
+ ## 默认静态服务:不需要配置或 SDK
8
+
9
+ ```tsx
10
+ <Icon name="tabler:star" />
11
+ // GET https://tabler.icones.go-slim.dev/data/star.json
12
+ ```
13
+
14
+ 默认只接受明确的 `set:name`,不会猜测裸名称,也不会转发 provider 名称。响应是单图标 tuple JSON,CSR 获取后内联渲染;SSR 可通过 controller、Astro 或每请求 store 预加载。`createIconesIconLoader` 可以复用该协议并覆盖 `fetch`、请求头或域名:
15
+
16
+ ```ts
17
+ import { createIconesIconLoader } from "@icones/core"
18
+
19
+ const loader = createIconesIconLoader({
20
+ domain: "icones.go-slim.dev",
21
+ requestInit: { cache: "force-cache" },
22
+ })
23
+ ```
24
+
25
+ ## 自建静态资源
26
+
27
+ ```ts
28
+ import { createStaticIconLoader, createIconStore } from "@icones/core"
29
+
30
+ const store = createIconStore({
31
+ api: createStaticIconLoader("/icons"),
32
+ })
33
+ await store.preload(["tabler:star"])
34
+ // GET /icons/tabler/data/star.json
35
+ ```
36
+
37
+ 也可以直接将 `createStaticIconLoader("/icons")` 传给 `IconConfig.api` 或 `createIconScope({ api })`,覆盖默认服务。所有适配器都重新导出这两个助手。自定义 `fetch`、请求头和取消信号均受支持;SSR 自建服务使用绝对 base URL、每请求创建 store,并通过框架安全传递 `store.snapshot()` 给客户端的 `initialData`。
38
+
39
+ `createIconApiLoader({ baseUrl })` 保留已有 `/<set>.json?icons=<name>` 集合接口协议;显式选项省略 baseUrl 时使用本地 `/icons`,不会使用公共 API。静态托管请选择 `createStaticIconLoader` 或自定义 `url`,不要把集合接口当成静态文件地址。
40
+
41
+ ## 自定义服务:注入函数
42
+
43
+ ```ts
44
+ import { createIconApiLoader, createIconStore, type Data } from "@icones/core"
45
+
46
+ const store = createIconStore({
47
+ api: createIconApiLoader({
48
+ url: (name) => `/my-api/icons/${encodeURIComponent(name)}`,
49
+ transform: (json) => (json as { artwork: Data }).artwork,
50
+ }),
51
+ })
52
+ ```
53
+
54
+ `transform(json, name)` 可以是异步函数。也可直接提供 `api: async (name, parsed, request) => data`,或者用 `sources` 回调加载本地模块;返回单个 `IconData` / tuple,找不到时返回 `null`。将 `request?.signal` 传入自定义 fetch,才能取消过期请求。通用 API 助手会处理 HTTP 错误、404 和取消信号,store 统一处理并发、缓存及 SSR 快照。
55
+
56
+ ## 确实需要 Iconify 时:由应用选择
57
+
58
+ 优先在 `scripts/icon-builder` 或 `@icones/vite` 中将上游格式转换为 tuple,运行时只消费生成资源。若应用确实需要 SDK 的额外解析能力,由应用自行安装并在函数边界内适配:
59
+
60
+ ```ts
61
+ import { createIconStore } from "@icones/core"
62
+ // 可选的应用依赖;不是 Core 或适配器的依赖。
63
+ import { getIconData, quicklyValidateIconSet } from "@iconify/utils"
64
+
65
+ const store = createIconStore({
66
+ api: {
67
+ url: (_name, parsed) =>
68
+ `https://api.iconify.design/${encodeURIComponent(parsed!.prefix)}.json?icons=${encodeURIComponent(parsed!.name)}`,
69
+ transform: (json, name) => {
70
+ const set = quicklyValidateIconSet(json)
71
+ return set
72
+ ? getIconData(set, name.slice(name.lastIndexOf(":") + 1))
73
+ : null
74
+ },
75
+ },
76
+ })
77
+ ```
78
+
79
+ 此示例只用于明确的 `set:name`。裸名称、provider 或其他服务协议请自行实现 `url`/加载器。如果只需要基础单图标/集合响应,显式设置该服务的 baseUrl 即可,不必安装 SDK。
80
+
81
+ 迁移注意:原 `createIconifyApiLoader` 与 `IconifyApiLoaderOptions` 已移除;改用通用助手或应用回调(请求选项类型为 `IconRequestOptions`)。Vite 的 `fallbackToApi` / `apiBaseUrl` 现在只用于 dev/build,不再决定 CSR/SSR 网络回退。
package/UTILITIES.md ADDED
@@ -0,0 +1,30 @@
1
+ # Core 的基础工具
2
+
3
+ 原 utils 的公共工具与类型现由 Core 直接维护,不再需要单独安装工具包。工具子入口不依赖框架、图标数据、文件 I/O 或 XML 解析器,也不加载组件状态。`IconData`、`IconSet` 是自有结构类型,不引用 `@iconify/types`。
4
+
5
+ - `set-options`:解析按图标集配置的选项,支持 `default` 回退和父子配置合并;只读取对象自身属性。
6
+ - `sizes`:根据用户配置推导默认与自定义尺寸名称,提供 CSS 尺寸类型。
7
+ - `element-types`:描述 SVG 元素元组及属性,不包含渲染或文件读取逻辑。
8
+ - `elements`:校验元素元组、转换 SVG/元组属性名;无需 React 或 DOM,校验结构不等同于过滤不可信 SVG。
9
+ - `view-box`:解析四个有限坐标值,要求宽高为正;无效输入返回 `null`,由调用方提供错误信息。
10
+ - `slug`:将字符串转成 ASCII slug;空结果由调用方决定是拒绝还是使用默认分类。
11
+ - `icon-data`:自有数据类型、结构判别、`readIconData` / `readIconSet` 校验及本地集合继承解析;不解析 SVG 或提供上游服务。
12
+ - `svg-data`:tuple → SVG body 的无 DOM 序列化、轻量 `renderSvgData` 视口/旋转/翻转处理,以及 symbol 文档包装。
13
+ - `svg`:SVG ID 引用改写与描边字符串处理;CSS 变量由调用方提供。
14
+
15
+ 资源管理工具只通过独立子入口导出,不从 Core 根入口重新导出,组件运行时不导入它们:
16
+
17
+ - `resource-types`:manifest、来源、目录记录和基础查询结果类型,不带网站 HTTP envelope。
18
+ - 样式分类与显示分离:manifest 的 `variantAliases` 展开为记录的 `variantAlias`、样式 facet 的 `alias`。查询和排序只使用 `variant` / `id`;跨集合混合了不同 alias 时不随意选择显示名称。上游映射由 Vite 构建预设提供,Core 不依赖任何具体集合或 Iconify。
19
+ - `manifest`:统一校验、条目展开、创建、更新和稳定序列化。无上游名称或内置别名;序列化顺序由调用方显式传入。
20
+ - `catalog`:筛选、计数和带缓存的查询索引。默认返回全部匹配项、字母排序且不保留空样式;`defaultLimit`、`maxLimit`、`variantOrder`、`includeEmptyVariants` 由网站或工具配置。`variantOrderBySet` 可覆盖某个集合的排序,避免把不同集合的样式名称混为全局规则;这些顺序只排序已有 facet,不创建不存在的样式。
21
+
22
+ 源码分组见 [源码结构](./src/README.md):基础类型和校验在 `data/`,SVG 工具在 `svg/`,配置工具在 `options/`,目录与 manifest 在 `resources/`。公开子入口不随内部目录变化。
23
+
24
+ 网站请求客户端与展示策略归 `app/src/features/catalog/`。上游名称映射和内置别名归 Vite tooling 的集合生产预设;开发/构建期文件读写由 Vite 管理,MCP 独立管理只读文件访问,不能将这些依赖反向引入 Core。
25
+
26
+ 对象本身也可作为选项值时,调用方应向选项解析与合并函数传入判别函数,区分共享对象值与按图标集索引的映射。
27
+
28
+ 各模块通过 `@icones/core/<模块名>` 独立导入,发布产物统一为 `.js` 与 `.d.ts`;工作区的 `development` / `bun` 条件仍直接使用源码。已有函数签名、排序策略与 Flag viewBox 等兼容规则不变。旧 `@icones/utils` 导入应改为对应的 Core 入口;没有保留转发包。
29
+
30
+ 根入口继续提供组件运行时和基础数据/SVG 工具。资源工具应明确导入 `@icones/core/catalog`、`@icones/core/manifest` 或 `@icones/core/resource-types`。其中资源来源类型 `IconSource` 与 Core 根入口的图标输入类型同名但含义不同,必须从资源子入口导入。
@@ -0,0 +1,17 @@
1
+ import { CatalogFacet, CatalogIcon, CatalogQuery, CatalogResult } from "./resource-types.js";
2
+ //#region src/resources/catalog.d.ts
3
+ /** Policies belong to the caller; omitted pagination returns all matching records. */
4
+ type CatalogOptions = {
5
+ defaultLimit?: number;
6
+ maxLimit?: number;
7
+ variantOrder?: readonly string[];
8
+ /** Set-specific presentation order; unknown sets use variantOrder/alphabetical order. */
9
+ variantOrderBySet?: Readonly<Record<string, readonly string[]>>;
10
+ includeEmptyVariants?: boolean;
11
+ };
12
+ /** Shared filtering and pagination for static browsers, servers and build tools. */
13
+ declare function selectCatalog(records: readonly CatalogIcon[], query?: CatalogQuery, options?: CatalogOptions): CatalogResult;
14
+ /** Immutable metadata snapshot with a bounded LRU of filters, shared by pages. */
15
+ declare function createCatalogIndex(records: readonly CatalogIcon[], maxQueries?: number, options?: CatalogOptions): (query?: CatalogQuery) => CatalogResult;
16
+ //#endregion
17
+ export { type CatalogFacet, type CatalogIcon, CatalogOptions, type CatalogQuery, type CatalogResult, createCatalogIndex, selectCatalog };
@@ -0,0 +1,115 @@
1
+ //#region src/resources/catalog.ts
2
+ function normalizeOptions(options) {
3
+ for (const key of ["defaultLimit", "maxLimit"]) {
4
+ const value = options[key];
5
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new RangeError(`Invalid catalog ${key}: ${value}`);
6
+ }
7
+ return {
8
+ ...options,
9
+ variantOrder: [...options.variantOrder ?? []],
10
+ variantOrderBySet: Object.fromEntries(Object.entries(options.variantOrderBySet ?? {}).map(([set, order]) => [set, [...order]]))
11
+ };
12
+ }
13
+ /** Shared filtering and pagination for static browsers, servers and build tools. */
14
+ function selectCatalog(records, query = {}, options = {}) {
15
+ const policy = normalizeOptions(options);
16
+ return page(select(records, query, false, policy), query, policy);
17
+ }
18
+ function termsFor(query) {
19
+ return (query.q ?? "").toLowerCase().trim().split(/\s+/).filter(Boolean);
20
+ }
21
+ function select(records, query, sorted = false, options = {}) {
22
+ const terms = termsFor(query);
23
+ const found = records.filter((record) => terms.every((term) => `${record.name} ${record.category}`.includes(term)) && (!query.suffix || record.name.endsWith(query.suffix)) && (!query.excludeSuffix || !record.name.endsWith(query.excludeSuffix)));
24
+ const selectedSet = found.filter((record) => !query.set || record.prefix === query.set);
25
+ const selectedVariant = selectedSet.filter((record) => !query.variant || record.variant === query.variant);
26
+ const selected = selectedVariant.filter((record) => !query.category || record.category === query.category);
27
+ if (!sorted) selected.sort((a, b) => a.name.localeCompare(b.name));
28
+ const variantRecords = options.includeEmptyVariants ? records.filter((record) => !query.set || record.prefix === query.set) : selectedSet;
29
+ return {
30
+ selected,
31
+ sets: facets(found, "prefix"),
32
+ categories: facets(selectedVariant, "category"),
33
+ variants: facets(variantRecords, "variant", query.set && options.variantOrderBySet && Object.hasOwn(options.variantOrderBySet, query.set) ? options.variantOrderBySet[query.set] : options.variantOrder).map(({ id }) => {
34
+ const aliases = new Set(variantRecords.filter((record) => record.variant === id).map((record) => record.variantAlias ?? id));
35
+ const alias = aliases.size === 1 ? aliases.values().next().value : void 0;
36
+ return {
37
+ id,
38
+ count: selectedSet.filter((record) => record.variant === id).length,
39
+ ...alias && alias !== id ? { alias } : {}
40
+ };
41
+ })
42
+ };
43
+ }
44
+ function page(selection, query, options) {
45
+ const { selected, sets, categories, variants } = selection;
46
+ const limit = Math.min(options.maxLimit ?? Infinity, Math.max(1, query.limit ?? options.defaultLimit ?? selected.length));
47
+ const offset = Math.max(0, query.offset ?? 0);
48
+ return {
49
+ icons: selected.slice(offset, offset + limit).map(({ name, prefix, category, variant, variantAlias }) => ({
50
+ name,
51
+ prefix,
52
+ category,
53
+ ...variant ? { variant } : {},
54
+ ...variantAlias ? { variantAlias } : {}
55
+ })),
56
+ sets: sets.map(({ id, count }) => ({
57
+ id,
58
+ count
59
+ })),
60
+ categories: categories.map(({ id, count }) => ({
61
+ id,
62
+ count
63
+ })),
64
+ ...variants.length ? { variants: variants.map((facet) => ({ ...facet })) } : {},
65
+ total: selected.length,
66
+ offset,
67
+ nextOffset: offset + limit < selected.length ? offset + limit : null
68
+ };
69
+ }
70
+ /** Immutable metadata snapshot with a bounded LRU of filters, shared by pages. */
71
+ function createCatalogIndex(records, maxQueries = 32, options = {}) {
72
+ if (!Number.isSafeInteger(maxQueries) || maxQueries < 0) throw new RangeError(`Invalid catalog query cache capacity: ${maxQueries}`);
73
+ const policy = normalizeOptions(options);
74
+ const sorted = records.map(({ name, prefix, category, variant, variantAlias }) => ({
75
+ name,
76
+ prefix,
77
+ category,
78
+ ...variant ? { variant } : {},
79
+ ...variantAlias ? { variantAlias } : {}
80
+ })).sort((a, b) => a.name.localeCompare(b.name));
81
+ const queries = /* @__PURE__ */ new Map();
82
+ return (query = {}) => {
83
+ const key = JSON.stringify([
84
+ termsFor(query),
85
+ query.set || "",
86
+ query.category || "",
87
+ query.variant || "",
88
+ query.suffix || "",
89
+ query.excludeSuffix || ""
90
+ ]);
91
+ let selection = queries.get(key);
92
+ if (selection) queries.delete(key);
93
+ else selection = select(sorted, query, true, policy);
94
+ if (maxQueries > 0) {
95
+ queries.set(key, selection);
96
+ if (queries.size > maxQueries) queries.delete(queries.keys().next().value);
97
+ }
98
+ return page(selection, query, policy);
99
+ };
100
+ }
101
+ function facets(records, key, order = []) {
102
+ const counts = /* @__PURE__ */ new Map();
103
+ for (const record of records) {
104
+ const id = record[key];
105
+ if (id) counts.set(id, (counts.get(id) ?? 0) + 1);
106
+ }
107
+ return [...counts].sort(([a], [b]) => {
108
+ return (key === "variant" ? (order.indexOf(a) < 0 ? 99 : order.indexOf(a)) - (order.indexOf(b) < 0 ? 99 : order.indexOf(b)) : 0) || a.localeCompare(b);
109
+ }).map(([id, count]) => ({
110
+ id,
111
+ count
112
+ }));
113
+ }
114
+ //#endregion
115
+ export { createCatalogIndex, selectCatalog };
@@ -0,0 +1,27 @@
1
+ import { IconLoadState } from "./types.js";
2
+ import { IconStore, IconStoreOptions } from "./store.js";
3
+ import { IconAppearance, IconOptions } from "./presentation.js";
4
+ //#region src/runtime/controller.d.ts
5
+ type IconScopeOptions = IconAppearance & Pick<IconStoreOptions, "sources" | "api"> & {
6
+ store?: IconStore;
7
+ };
8
+ type IconScope = {
9
+ readonly store: IconStore;
10
+ readonly appearance: IconAppearance;
11
+ };
12
+ /** Explicit scopes isolate SSR requests. Size-only child scopes share the parent's store. */
13
+ declare function createIconScope(options?: IconScopeOptions, parent?: IconScope, previous?: IconScope): IconScope;
14
+ /** Memoize a provider's store while its presentation options change. */
15
+ declare function createIconScopeResolver(): (options?: IconScopeOptions, parent?: IconScope) => IconScope;
16
+ declare function defaultIconScope(): IconScope;
17
+ type IconController = {
18
+ getState(): IconLoadState;
19
+ update(props: IconOptions, scope?: IconScope): void;
20
+ subscribe(listener: () => void): () => void;
21
+ load(): Promise<IconLoadState>;
22
+ destroy(): void;
23
+ };
24
+ /** No subscriptions or requests until subscribe/load. Safe to instantiate during SSR. */
25
+ declare function createIconController(initial: IconOptions, initialScope?: IconScope): IconController;
26
+ //#endregion
27
+ export { IconController, IconScope, IconScopeOptions, createIconController, createIconScope, createIconScopeResolver, defaultIconScope };
@@ -0,0 +1,156 @@
1
+ import { isIconData } from "./icon-data.js";
2
+ import { mergeIconApi } from "./loaders.js";
3
+ import { iconLoader, resolve } from "./runtime.js";
4
+ import { createIconStore } from "./store.js";
5
+ import { registeredName, registeredSources, registryStore } from "./registry.js";
6
+ import { createIconSourceValidator, defaultIconAppearance, mergeIconAppearance, selectIconSource } from "./presentation.js";
7
+ //#region src/runtime/controller.ts
8
+ const inputs = /* @__PURE__ */ new WeakMap();
9
+ const defaultScopes = /* @__PURE__ */ new WeakSet();
10
+ /** Explicit scopes isolate SSR requests. Size-only child scopes share the parent's store. */
11
+ function createIconScope(options = {}, parent, previous) {
12
+ const before = previous && inputs.get(previous);
13
+ const same = before && before.sources === options.sources && before.api === options.api && before.parent === parent?.store && before.store === options.store;
14
+ const store = options.store ?? (same ? previous.store : options.sources === void 0 && options.api === void 0 && parent ? parent.store : createIconStore({
15
+ sources: options.sources ?? (parent ? void 0 : registeredSources),
16
+ parent: parent?.store,
17
+ api: mergeIconApi(parent ? void 0 : iconLoader, options.api)
18
+ }));
19
+ const inherited = parent?.appearance ?? defaultIconAppearance;
20
+ const scope = {
21
+ store,
22
+ appearance: mergeIconAppearance(inherited, options)
23
+ };
24
+ inputs.set(scope, {
25
+ sources: options.sources,
26
+ api: options.api,
27
+ parent: parent?.store,
28
+ store: options.store
29
+ });
30
+ if (parent && defaultScopes.has(parent) && options.sources === void 0 && options.api === void 0 && options.store === void 0) defaultScopes.add(scope);
31
+ return scope;
32
+ }
33
+ /** Memoize a provider's store while its presentation options change. */
34
+ function createIconScopeResolver() {
35
+ let previous;
36
+ return (options = {}, parent) => previous = createIconScope(options, parent, previous);
37
+ }
38
+ function defaultIconScope() {
39
+ const scope = typeof window === "undefined" ? createIconScope() : {
40
+ store: registryStore,
41
+ appearance: defaultIconAppearance
42
+ };
43
+ defaultScopes.add(scope);
44
+ return scope;
45
+ }
46
+ const missing = { status: "missing" };
47
+ /** No subscriptions or requests until subscribe/load. Safe to instantiate during SSR. */
48
+ function createIconController(initial, initialScope = defaultIconScope()) {
49
+ let props = initial;
50
+ let scope = initialScope;
51
+ let store = scope.store;
52
+ let ownedStore;
53
+ let loader = props.loader;
54
+ let sourceStore = scope.store;
55
+ let name;
56
+ let staticState;
57
+ let unsubscribe;
58
+ let version = 0;
59
+ let destroyed = false;
60
+ const listeners = /* @__PURE__ */ new Set();
61
+ const validateSources = createIconSourceValidator();
62
+ const getState = () => staticState ?? (name ? store.getState(name) : missing);
63
+ /** Rebuild selection, owned store and static inline state for latest props/scope. */
64
+ function prepare() {
65
+ validateSources(props);
66
+ if (props.loader !== loader || scope.store !== sourceStore || props.loader && !ownedStore) {
67
+ ownedStore?.invalidate();
68
+ loader = props.loader;
69
+ sourceStore = scope.store;
70
+ ownedStore = loader ? createIconStore({
71
+ parent: scope.store,
72
+ api: loader
73
+ }) : void 0;
74
+ }
75
+ store = ownedStore ?? scope.store;
76
+ const selected = selectIconSource(props);
77
+ name = typeof selected.source === "string" ? defaultScopes.has(scope) ? registeredName(selected.source) : selected.source : void 0;
78
+ staticState = void 0;
79
+ const source = selected.source;
80
+ if (typeof source === "string") {
81
+ const compiled = props.loader ? void 0 : resolve(source);
82
+ if (compiled) staticState = {
83
+ status: "loaded",
84
+ ...compiled
85
+ };
86
+ } else if (Array.isArray(source)) staticState = {
87
+ status: "loaded",
88
+ data: source,
89
+ fill: "none"
90
+ };
91
+ else if (isIconData(source)) staticState = {
92
+ status: "loaded",
93
+ data: source
94
+ };
95
+ }
96
+ function notify() {
97
+ for (const listener of listeners) listener();
98
+ }
99
+ /** Defer initial fetch until subscription stabilizes and props version still matches. */
100
+ function scheduleLoad() {
101
+ const expected = version;
102
+ queueMicrotask(() => {
103
+ if (!destroyed && listeners.size && expected === version && !staticState && name && getState().status === "idle") store.load(name).catch(() => {});
104
+ });
105
+ }
106
+ function bind() {
107
+ if (staticState || !name || !listeners.size) return;
108
+ unsubscribe = store.subscribe(name, () => {
109
+ notify();
110
+ scheduleLoad();
111
+ });
112
+ scheduleLoad();
113
+ }
114
+ prepare();
115
+ return {
116
+ getState,
117
+ update(next, nextScope = scope) {
118
+ if (destroyed) return;
119
+ version++;
120
+ unsubscribe?.();
121
+ unsubscribe = void 0;
122
+ props = next;
123
+ scope = nextScope;
124
+ prepare();
125
+ bind();
126
+ notify();
127
+ },
128
+ /** Register render listener and start loading when first listener appears. */
129
+ subscribe(listener) {
130
+ if (destroyed) throw new Error("Icon controller has been destroyed.");
131
+ listeners.add(listener);
132
+ if (listeners.size === 1) bind();
133
+ return () => {
134
+ listeners.delete(listener);
135
+ if (!listeners.size) {
136
+ unsubscribe?.();
137
+ unsubscribe = void 0;
138
+ }
139
+ };
140
+ },
141
+ async load() {
142
+ if (!destroyed && !staticState && name) await store.load(name);
143
+ return getState();
144
+ },
145
+ /** Stop subscriptions, drop queued loads and invalidate any inline-owned child store. */
146
+ destroy() {
147
+ destroyed = true;
148
+ version++;
149
+ unsubscribe?.();
150
+ listeners.clear();
151
+ ownedStore?.invalidate();
152
+ }
153
+ };
154
+ }
155
+ //#endregion
156
+ export { createIconController, createIconScope, createIconScopeResolver, defaultIconScope };
@@ -0,0 +1,18 @@
1
+ import { r as ElementData } from "./element-types-C9jOyda4.js";
2
+ import { a as IconTransform, n as IconData } from "./icon-data-HGvJGwoB.js";
3
+ //#region src/svg/data.d.ts
4
+ /** Wrap an already-rendered SVG body without parsing it or imposing collection rules. */
5
+ declare function createSvgSymbolDocument(body: string, viewBox: string, id?: string): string;
6
+ /** Serialize tuples for SVG, SSR and Iconify-compatible APIs. No DOM/parser in the client. */
7
+ declare function elementDataToIcon(data: ElementData, defaultFill?: string): IconData;
8
+ /**
9
+ * Minimal SVG viewport/quarter-turn renderer. No XML parsing, sizing presets or SDK.
10
+ * Body-only objects keep their historical 16×16 default; tuples carry explicit 24×24.
11
+ * Source transforms precede presentation transforms. SVG definitions remain intact.
12
+ */
13
+ declare function renderSvgData(data: IconData, options?: IconTransform): {
14
+ body: string;
15
+ viewBox: string;
16
+ };
17
+ //#endregion
18
+ export { elementDataToIcon as n, renderSvgData as r, createSvgSymbolDocument as t };