@octane-xplat/icons 0.0.1 → 0.10.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,171 @@
1
1
  # @octane-xplat/icons
2
2
 
3
- This is a placeholder stub published to reserve the package name ahead of the
4
- first automated release. It has no supported API.
3
+ Render bundled Iconify icons on web, iOS, Android, and experimental AppKit macOS. Apps choose their sets;
4
+ this package ships no icon data, default collection, or Iconify API client.
5
+
6
+ ## Install and register
7
+
8
+ In an Octane-xplat app with `@octane-xplat/ui` already configured:
9
+
10
+ ```sh
11
+ pnpm add @octane-xplat/icons @iconify-json/heroicons
12
+ ```
13
+
14
+ Register bundled JSON before mounting the app, then use `prefix:name`:
15
+
16
+ ```tsx
17
+ import { icons as heroicons } from '@iconify-json/heroicons'
18
+ import { addCollection, Icon } from '@octane-xplat/icons'
19
+
20
+ addCollection(heroicons)
21
+
22
+ export function NextAction() {
23
+ return <Icon name="heroicons:arrow-right" size={24} color="#2563eb" label="Next" />
24
+ }
25
+ ```
26
+
27
+ Heroicons is the Astryx-parity choice: Astryx's Icon is heroicons-based.
28
+ It is an example, not a default. Register any number of `@iconify-json/*`
29
+ collections the same way. A collection's `prefix` determines its namespace;
30
+ there is no automatic package discovery, fetching, or fallback set.
31
+
32
+ ```ts
33
+ // heroicons and addCollection are imported above.
34
+ addCollection(heroicons)
35
+ console.log(heroicons.prefix) // The prefix is used before the colon in name.
36
+ ```
37
+
38
+ Importing a whole collection bundles that collection. To keep a small bundle,
39
+ provide equivalent Iconify JSON containing only the icons you use:
40
+
41
+ ```ts
42
+ addCollection({
43
+ prefix: 'app',
44
+ width: 24,
45
+ height: 24,
46
+ icons: {
47
+ next: {
48
+ body: '<path fill="none" d="M4 12h16m-6-6 6 6-6 6" stroke="currentColor" stroke-width="2"/>',
49
+ },
50
+ },
51
+ })
52
+ ```
53
+
54
+ Keep collection dimensions and any parent icons needed by aliases when pruning
55
+ an existing set. Use only trusted bundled SVG data; conversion does not sanitize
56
+ SVG bodies. Treat registered JSON as immutable.
57
+
58
+ ## Rendering and updates
59
+
60
+ Import `Icon` from **this package**. It owns a collection registry separate from
61
+ UI's glyph registry: `addCollection` does not populate UI's `registerIcons` seam.
62
+ That seam has no change subscription and shares stored definition IDs across
63
+ instances. This component subscribes to registration changes and makes SVG IDs
64
+ unique for each instance. Re-registering a prefix replaces its whole collection
65
+ and updates mounted icons, including a previously missing icon.
66
+
67
+ ```tsx
68
+ import { addCollection, Icon } from '@octane-xplat/icons'
69
+
70
+ addCollection({ prefix: 'trip', icons: { next: { body: '<path d="M0 0h16v16H0z" />' } } })
71
+ export function TripIcon() {
72
+ return <Icon name="trip:next" />
73
+ }
74
+ // Replacing this collection notifies already-mounted TripIcon components.
75
+ addCollection({ prefix: 'trip', icons: { next: { body: '<circle cx="8" cy="8" r="8" />' } } })
76
+ ```
77
+
78
+ `resolveIcon('prefix:name')` returns normalized Iconify data, including collection
79
+ dimensions and alias transforms, or `undefined` for a missing/invalid name.
80
+ `Icon` renders nothing in that case. Alias cycles and missing parents also resolve
81
+ to nothing. No network request is made.
82
+
83
+ ```ts
84
+ import { resolveIcon } from '@octane-xplat/icons'
85
+
86
+ const icon = resolveIcon('heroicons:arrow-right')
87
+ if (!icon) console.log('Register this icon collection before rendering')
88
+ ```
89
+
90
+ | Prop | Behavior |
91
+ | ----------------- | ------------------------------------------------------------------------------------------------ |
92
+ | `name` | Required `prefix:name` |
93
+ | `size` | Positive finite height in logical pixels, default 24; width follows the transformed aspect ratio |
94
+ | `color` | Replaces `currentColor` and supplies default fill; explicit multicolor fills remain intact |
95
+ | `rotate` | Quarter turns; applied after the icon's own transforms |
96
+ | `hFlip`, `vFlip` | Additional horizontal/vertical flips |
97
+ | `id`, `className` | Passed to the rendered host |
98
+ | `label` | Accessible description; omit for decorative icons |
99
+
100
+ Web uses inline `<svg>` and inherits `currentColor` when `color` is omitted.
101
+ Native defaults to black; pass a concrete color to match app/theme text. It sends
102
+ DOM-free SVG markup through UI's `Image` SVG path to the vendored ui-svg SVGView
103
+ (SVGKit on iOS, androidsvg on Android). No extra native plugin setup is needed
104
+ beyond UI's setup. All leaves have the same props and exports. The package uses
105
+ style objects and ships no CSS pipeline. Native compatibility follows those SVG
106
+ engines; browser-only SVG features such as filters/animation are not guaranteed.
107
+
108
+ ```tsx
109
+ // Icon is imported from this package above.
110
+ export function Direction() {
111
+ return <Icon name="heroicons:arrow-right" color="#2563eb" rotate={1} hFlip label="Continue" />
112
+ }
113
+ ```
114
+
115
+ AppKit uses UI's macOS `Image` to encode inline SVG with Foundation and load it
116
+ into `NSImageView` through public `NSImage` APIs. This path was runtime-tested
117
+ on macOS 27.0.1; SVG decoding on the app deployment minimum macOS 13.5 remains
118
+ unverified. An OS that cannot decode the SVG produces an empty image; there is
119
+ no fallback renderer. macOS forwards `id`/`className`, but the image host does
120
+ not apply class-based styling. Labels map to the native accessibility label;
121
+ unlabeled images are marked decorative. Pixel fidelity and OS accessibility
122
+ traversal remain unverified. See [AppKit SVG notes](../../docs/notes/icon-svg-notes.md).
123
+
124
+ ```tsx
125
+ // Direction.macos.tsx
126
+ import { Icon } from '@octane-xplat/icons'
127
+
128
+ export function Direction() {
129
+ return <Icon id="next" name="heroicons:arrow-right" size={24} label="Next" />
130
+ }
131
+ ```
132
+
133
+ `iconToSvg(icon, options?)` exposes the shared DOM-free conversion. It returns
134
+ `body`, `viewBox`, numeric `width`/`height`, and complete `markup`. It resolves
135
+ transforms and rewrites definition references. For native markup, supply a
136
+ concrete `color`; the helper otherwise retains `currentColor`.
137
+
138
+ ```ts
139
+ import { iconToSvg, resolveIcon } from '@octane-xplat/icons'
140
+
141
+ const icon = resolveIcon('heroicons:arrow-right')
142
+ if (icon) {
143
+ const svg = iconToSvg(icon, { color: '#2563eb' })
144
+ console.log(svg.viewBox, svg.width, svg.height, svg.markup)
145
+ }
146
+ ```
147
+
148
+ ## Licenses
149
+
150
+ This package and Iconify's utilities are MIT. Icon sets retain their own licenses;
151
+ Iconify JSON does not grant a common license across sets. Heroicons is MIT.
152
+ Check each set's `info.json`, license files, and upstream license, and retain
153
+ required attribution/notices when bundling or pruning its data. Apps own that
154
+ license handling; this leaf distributes no set data.
155
+
156
+ ## Verification and example
157
+
158
+ The maintained [icon probe](../../examples/probes/icons.tsrx) exercises multiple
159
+ sets, sizing, aliases, missing names, and mounted registration/prop updates:
160
+
161
+ ```sh
162
+ pnpm probe doctor
163
+ pnpm probe run examples/probes/icons.macos.tsrx --target macos --deps @octane-xplat/icons
164
+ pnpm probe run examples/probes/icons.tsrx --target web --deps @octane-xplat/icons
165
+ pnpm probe run examples/probes/icons.tsrx --target ios --device SIMULATOR_UDID --deps @octane-xplat/icons
166
+ ```
167
+
168
+ For package checks, run `pnpm --filter @octane-xplat/icons build`, `typecheck`,
169
+ `test`, and `pack:check`. Runtime evidence is recorded separately from builds;
170
+ The probe proves host/source updates, not pixel parity or OS accessibility
171
+ traversal. Run it on each target you ship. This package owns the maintained probe.
@@ -0,0 +1,71 @@
1
+ import { registry, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { defineUniversalComponent, hookSlots, universalComponent, universalPlan, universalProps, universalValue, useMemo, withSlot } from "@nativescript-community/octane";
4
+ import { Image, useStore } from "@octane-xplat/ui";
5
+ //#region src/Icon.macos.tsrx
6
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
7
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
8
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
9
+ var __octaneUniversalPlan0 = universalPlan("macos", {
10
+ "kind": "slot",
11
+ "slot": 0
12
+ });
13
+ var Icon = defineUniversalComponent("macos", function Icon(props) {
14
+ withSlot(_h$0, useStore, registry);
15
+ const icon = resolveIcon(props.name);
16
+ const image = useMemo(() => {
17
+ if (!icon) return void 0;
18
+ const svg = iconToSvg(icon, {
19
+ size: props.size,
20
+ color: props.color ?? "#000000",
21
+ rotate: props.rotate,
22
+ hFlip: props.hFlip,
23
+ vFlip: props.vFlip
24
+ });
25
+ return {
26
+ src: svg.markup,
27
+ width: svg.width,
28
+ height: svg.height
29
+ };
30
+ }, [
31
+ icon,
32
+ props.size,
33
+ props.color,
34
+ props.rotate,
35
+ props.hFlip,
36
+ props.vFlip
37
+ ], _h$1);
38
+ if (!image) return null;
39
+ return universalValue(__octaneUniversalPlan0, [universalComponent("macos", Image, universalProps([
40
+ [
41
+ "set",
42
+ "id",
43
+ props.id
44
+ ],
45
+ [
46
+ "set",
47
+ "className",
48
+ props.className
49
+ ],
50
+ [
51
+ "set",
52
+ "src",
53
+ image.src
54
+ ],
55
+ [
56
+ "set",
57
+ "alt",
58
+ props.label ?? ""
59
+ ],
60
+ [
61
+ "set",
62
+ "style",
63
+ {
64
+ width: image.width,
65
+ height: image.height
66
+ }
67
+ ]
68
+ ]))]);
69
+ }, { "module": "@octane-xplat/macos-renderer" });
70
+ //#endregion
71
+ export { Icon };
@@ -0,0 +1,4 @@
1
+ import { addCollection, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { Icon } from "./Icon.macos.js";
4
+ export { Icon, addCollection, iconToSvg, resolveIcon };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "c4bfdd89-aba2-4700-a0cb-03dc4a95d97b",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,45 @@
1
+ import { getIconData } from "@iconify/utils/lib/icon-set/get-icon";
2
+ //#region src/registry.ts
3
+ var collections = /* @__PURE__ */ new Map();
4
+ var cache = /* @__PURE__ */ new Map();
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ var revision = 0;
7
+ var identifier = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ var registry = {
9
+ get: () => revision,
10
+ subscribe(notify) {
11
+ listeners.add(notify);
12
+ return () => {
13
+ listeners.delete(notify);
14
+ };
15
+ }
16
+ };
17
+ /**
18
+ * Register trusted, bundled Iconify JSON. Replaces the collection with the same
19
+ * prefix and updates mounted Icons. No fetching or default set is provided.
20
+ * Treat the supplied JSON as immutable after registration.
21
+ * @throws TypeError if the prefix or icons dictionary is missing/invalid.
22
+ */
23
+ function addCollection(collection) {
24
+ if (!collection || typeof collection.prefix !== "string" || !identifier.test(collection.prefix) || !collection.icons || typeof collection.icons !== "object" || Array.isArray(collection.icons)) throw new TypeError("Expected Iconify JSON with a non-empty prefix and icons dictionary");
25
+ collections.set(collection.prefix, {
26
+ ...collection,
27
+ icons: Object.assign(Object.create(null), collection.icons),
28
+ aliases: Object.assign(Object.create(null), collection.aliases)
29
+ });
30
+ cache.clear();
31
+ revision++;
32
+ for (const notify of [...listeners]) notify();
33
+ }
34
+ /** Resolve `prefix:name`, including aliases and collection dimensions; missing/invalid names return undefined. */
35
+ function resolveIcon(name) {
36
+ if (cache.has(name)) return cache.get(name);
37
+ const parts = name.split(":");
38
+ if (parts.length !== 2 || !parts.every((part) => identifier.test(part))) return;
39
+ const collection = collections.get(parts[0]);
40
+ const icon = collection ? getIconData(collection, parts[1]) ?? void 0 : void 0;
41
+ cache.set(name, icon);
42
+ return icon;
43
+ }
44
+ //#endregion
45
+ export { addCollection, registry, resolveIcon };
@@ -0,0 +1,37 @@
1
+ import { iconToSVG } from "@iconify/utils/lib/svg/build";
2
+ import { replaceIDs } from "@iconify/utils/lib/svg/id";
3
+ //#region src/svg.ts
4
+ function escapeAttribute(value) {
5
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
6
+ }
7
+ /**
8
+ * Convert trusted icon data to SVG without DOM APIs. Applies transforms, keeps
9
+ * multicolor fills, and gives definitions unique IDs on every invocation.
10
+ * Without a color, markup uses currentColor; native callers should supply a
11
+ * concrete tint. This is a converter, not an SVG sanitizer.
12
+ * @throws RangeError if size is not finite and positive.
13
+ */
14
+ function iconToSvg(icon, options = {}) {
15
+ const size = options.size ?? 24;
16
+ if (!Number.isFinite(size) || size <= 0) throw new RangeError("Icon size must be finite and positive");
17
+ const result = iconToSVG(icon, {
18
+ height: size,
19
+ rotate: options.rotate ?? 0,
20
+ hFlip: options.hFlip ?? false,
21
+ vFlip: options.vFlip ?? false
22
+ });
23
+ const color = escapeAttribute(options.color ?? "currentColor");
24
+ const body = replaceIDs(result.body).replace(/currentColor/g, () => color);
25
+ const viewBox = result.attributes.viewBox;
26
+ const width = Number(result.attributes.width);
27
+ const height = Number(result.attributes.height);
28
+ return {
29
+ body,
30
+ viewBox,
31
+ width,
32
+ height,
33
+ markup: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" width="${width}" height="${height}" color="${color}" fill="${color}">${body}</svg>`
34
+ };
35
+ }
36
+ //#endregion
37
+ export { iconToSvg };
@@ -0,0 +1,79 @@
1
+ import { registry, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { defineUniversalComponent, hookSlots, universalComponent, universalPlan, universalProps, universalValue, useMemo, withSlot } from "@nativescript-community/octane";
4
+ import { Image, useStore } from "@octane-xplat/ui";
5
+ //#region src/Icon.tsrx
6
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
7
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
8
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
9
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
10
+ "kind": "slot",
11
+ "slot": 0
12
+ });
13
+ var Icon = defineUniversalComponent("nativescript", function Icon(props) {
14
+ withSlot(_h$0, useStore, registry);
15
+ const icon = resolveIcon(props.name);
16
+ const svg = useMemo(() => icon ? iconToSvg(icon, {
17
+ size: props.size,
18
+ color: props.color ?? "#000000",
19
+ rotate: props.rotate,
20
+ hFlip: props.hFlip,
21
+ vFlip: props.vFlip
22
+ }) : void 0, [
23
+ icon,
24
+ props.size,
25
+ props.color,
26
+ props.rotate,
27
+ props.hFlip,
28
+ props.vFlip
29
+ ], _h$1);
30
+ if (!svg) return null;
31
+ return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", Image, universalProps([
32
+ [
33
+ "set",
34
+ "id",
35
+ props.id
36
+ ],
37
+ [
38
+ "set",
39
+ "className",
40
+ props.className
41
+ ],
42
+ [
43
+ "set",
44
+ "src",
45
+ svg.markup
46
+ ],
47
+ [
48
+ "set",
49
+ "alt",
50
+ props.label
51
+ ],
52
+ [
53
+ "set",
54
+ "ios",
55
+ {
56
+ accessible: !!props.label,
57
+ accessibilityRole: "image"
58
+ }
59
+ ],
60
+ [
61
+ "set",
62
+ "android",
63
+ {
64
+ accessible: !!props.label,
65
+ accessibilityRole: "image"
66
+ }
67
+ ],
68
+ [
69
+ "set",
70
+ "style",
71
+ {
72
+ width: svg.width,
73
+ height: svg.height
74
+ }
75
+ ]
76
+ ]))]);
77
+ }, { "module": "@nativescript-community/octane" });
78
+ //#endregion
79
+ export { Icon };
@@ -0,0 +1,4 @@
1
+ import { addCollection, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { Icon } from "./Icon.js";
4
+ export { Icon, addCollection, iconToSvg, resolveIcon };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "32b529c5-58bd-4ae2-9bc8-42cf21e6fbdc",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,45 @@
1
+ import { getIconData } from "@iconify/utils/lib/icon-set/get-icon";
2
+ //#region src/registry.ts
3
+ var collections = /* @__PURE__ */ new Map();
4
+ var cache = /* @__PURE__ */ new Map();
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ var revision = 0;
7
+ var identifier = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ var registry = {
9
+ get: () => revision,
10
+ subscribe(notify) {
11
+ listeners.add(notify);
12
+ return () => {
13
+ listeners.delete(notify);
14
+ };
15
+ }
16
+ };
17
+ /**
18
+ * Register trusted, bundled Iconify JSON. Replaces the collection with the same
19
+ * prefix and updates mounted Icons. No fetching or default set is provided.
20
+ * Treat the supplied JSON as immutable after registration.
21
+ * @throws TypeError if the prefix or icons dictionary is missing/invalid.
22
+ */
23
+ function addCollection(collection) {
24
+ if (!collection || typeof collection.prefix !== "string" || !identifier.test(collection.prefix) || !collection.icons || typeof collection.icons !== "object" || Array.isArray(collection.icons)) throw new TypeError("Expected Iconify JSON with a non-empty prefix and icons dictionary");
25
+ collections.set(collection.prefix, {
26
+ ...collection,
27
+ icons: Object.assign(Object.create(null), collection.icons),
28
+ aliases: Object.assign(Object.create(null), collection.aliases)
29
+ });
30
+ cache.clear();
31
+ revision++;
32
+ for (const notify of [...listeners]) notify();
33
+ }
34
+ /** Resolve `prefix:name`, including aliases and collection dimensions; missing/invalid names return undefined. */
35
+ function resolveIcon(name) {
36
+ if (cache.has(name)) return cache.get(name);
37
+ const parts = name.split(":");
38
+ if (parts.length !== 2 || !parts.every((part) => identifier.test(part))) return;
39
+ const collection = collections.get(parts[0]);
40
+ const icon = collection ? getIconData(collection, parts[1]) ?? void 0 : void 0;
41
+ cache.set(name, icon);
42
+ return icon;
43
+ }
44
+ //#endregion
45
+ export { addCollection, registry, resolveIcon };
@@ -0,0 +1,37 @@
1
+ import { iconToSVG } from "@iconify/utils/lib/svg/build";
2
+ import { replaceIDs } from "@iconify/utils/lib/svg/id";
3
+ //#region src/svg.ts
4
+ function escapeAttribute(value) {
5
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
6
+ }
7
+ /**
8
+ * Convert trusted icon data to SVG without DOM APIs. Applies transforms, keeps
9
+ * multicolor fills, and gives definitions unique IDs on every invocation.
10
+ * Without a color, markup uses currentColor; native callers should supply a
11
+ * concrete tint. This is a converter, not an SVG sanitizer.
12
+ * @throws RangeError if size is not finite and positive.
13
+ */
14
+ function iconToSvg(icon, options = {}) {
15
+ const size = options.size ?? 24;
16
+ if (!Number.isFinite(size) || size <= 0) throw new RangeError("Icon size must be finite and positive");
17
+ const result = iconToSVG(icon, {
18
+ height: size,
19
+ rotate: options.rotate ?? 0,
20
+ hFlip: options.hFlip ?? false,
21
+ vFlip: options.vFlip ?? false
22
+ });
23
+ const color = escapeAttribute(options.color ?? "currentColor");
24
+ const body = replaceIDs(result.body).replace(/currentColor/g, () => color);
25
+ const viewBox = result.attributes.viewBox;
26
+ const width = Number(result.attributes.width);
27
+ const height = Number(result.attributes.height);
28
+ return {
29
+ body,
30
+ viewBox,
31
+ width,
32
+ height,
33
+ markup: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" width="${width}" height="${height}" color="${color}" fill="${color}">${body}</svg>`
34
+ };
35
+ }
36
+ //#endregion
37
+ export { iconToSvg };
@@ -0,0 +1,75 @@
1
+ import { registry, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { bag12, clone, hookSlots, ifBlock, queueRefAttach, queueRefDetach, setAriaAttribute, setStyleProperty, template, withSlot } from "octane";
4
+ import { bindSignalAttribute, compilerMemoRegion, enableSignalBindings, hookMemoCreate, hookMemoEqual, hookMemoPublish, replaceRef, setPlainAttribute } from "octane/internal/client";
5
+ import { useStore } from "@octane-xplat/ui";
6
+ //#region src/Icon.web.tsrx
7
+ enableSignalBindings(1, true);
8
+ var _t$0 = /* @__PURE__ */ template("<svg style=\"display: inline-block; flex-shrink: 0;\"></svg>", 1);
9
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
10
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
11
+ var _rm$1 = [
12
+ "r",
13
+ "l",
14
+ "b"
15
+ ];
16
+ function __then$0(__props, __s, __extra) {
17
+ const props = __extra[0];
18
+ const svg = __extra[1];
19
+ let _b = __s.slots[0];
20
+ if (_b === void 0) {
21
+ let _m1, _m11;
22
+ const _root = clone(_t$0, void 0, "||");
23
+ _m1 = _root;
24
+ queueRefAttach(__s, _m11 = (element) => {
25
+ if (element) element.innerHTML = svg.body;
26
+ }, _m1);
27
+ (__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.l, _b.b));
28
+ _b = bag12(__s, _root, void 0, _m1, void 0, void 0, void 0, void 0, void 0, __s, void 0, void 0, void 0, _m11);
29
+ __s.refFields = _rm$1;
30
+ } else {
31
+ const _r = (element) => {
32
+ if (element) element.innerHTML = svg.body;
33
+ };
34
+ if (_r !== _b.l) _b.l = replaceRef(__s, _b.l, _r, _b.b);
35
+ }
36
+ _b.a = bindSignalAttribute(__s, _b.a, _b.b, "id", props.id, "b:5e515cc8", setPlainAttribute);
37
+ _b.c = bindSignalAttribute(__s, _b.c, _b.b, "class", props.className, "b:55ac2a96", "class");
38
+ _b.d = bindSignalAttribute(__s, _b.d, _b.b, "viewBox", svg.viewBox, "b:056826c3", setPlainAttribute);
39
+ _b.e = bindSignalAttribute(__s, _b.e, _b.b, "width", svg.width, "b:73d5f188", setPlainAttribute);
40
+ _b.f = bindSignalAttribute(__s, _b.f, _b.b, "height", svg.height, "b:3b5779f4", setPlainAttribute);
41
+ _b.g = bindSignalAttribute(__s, _b.g, _b.b, "fill", props.color ?? "currentColor", "b:13d0b8b1", setPlainAttribute);
42
+ {
43
+ const _v = props.color;
44
+ if (_b.h !== _v) {
45
+ setStyleProperty(_b.b, "color", _v, "display: inline-block; flex-shrink: 0;", _b.h);
46
+ _b.h = _v;
47
+ }
48
+ }
49
+ _b.i = bindSignalAttribute(__s, _b.i, _b.b, "role", props.label ? "img" : void 0, "b:23c7db75", setPlainAttribute);
50
+ _b.j = bindSignalAttribute(__s, _b.j, _b.b, "aria-label", props.label, "b:359cbba3", setAriaAttribute);
51
+ _b.k = bindSignalAttribute(__s, _b.k, _b.b, "aria-hidden", props.label ? void 0 : "true", "b:7db4983a", setAriaAttribute);
52
+ }
53
+ var Icon = function Icon(props, __s, __extra) {
54
+ const __block = __s.block;
55
+ const __memoRegion = compilerMemoRegion(__s, _hs$ + 0);
56
+ let __hk = __memoRegion.hooks;
57
+ if (__hk === void 0) __memoRegion.hooks = __hk = hookMemoCreate(8);
58
+ let __hkd0, __hkd1, __hkd2, __hkd3, __hkd4, __hkd5;
59
+ withSlot(_h$0, useStore, registry);
60
+ const icon = resolveIcon(props.name);
61
+ const svg = (__hkd0 = icon, __hkd1 = props.size, __hkd2 = props.color, __hkd3 = props.rotate, __hkd4 = props.hFlip, __hkd5 = props.vFlip, __hk[0] !== true || !hookMemoEqual(__hk[1], __hkd0) || !hookMemoEqual(__hk[2], __hkd1) || !hookMemoEqual(__hk[3], __hkd2) || !hookMemoEqual(__hk[4], __hkd3) || !hookMemoEqual(__hk[5], __hkd4) || !hookMemoEqual(__hk[6], __hkd5) ? hookMemoPublish(__hk, 0, icon ? iconToSvg(icon, {
62
+ size: props.size,
63
+ color: props.color,
64
+ rotate: props.rotate,
65
+ hFlip: props.hFlip,
66
+ vFlip: props.vFlip
67
+ }) : void 0, __hkd0, __hkd1, __hkd2, __hkd3, __hkd4, __hkd5) : __hk[7]);
68
+ if (__s.block.pending && !__s.block.crossRenderUpdate) return;
69
+ {
70
+ const __ifCondition = !!svg;
71
+ ifBlock(__s, 0, __block.parentNode, __ifCondition, __then$0, null, __block.endMarker, __ifCondition ? [props, svg] : void 0);
72
+ }
73
+ };
74
+ //#endregion
75
+ export { Icon };
@@ -0,0 +1,4 @@
1
+ import { addCollection, resolveIcon } from "./registry.js";
2
+ import { iconToSvg } from "./svg.js";
3
+ import { Icon } from "./Icon.web.js";
4
+ export { Icon, addCollection, iconToSvg, resolveIcon };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "2a59e429-04fb-4cd7-9912-0d22f2f8329a",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,45 @@
1
+ import { getIconData } from "@iconify/utils/lib/icon-set/get-icon";
2
+ //#region src/registry.ts
3
+ var collections = /* @__PURE__ */ new Map();
4
+ var cache = /* @__PURE__ */ new Map();
5
+ var listeners = /* @__PURE__ */ new Set();
6
+ var revision = 0;
7
+ var identifier = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ var registry = {
9
+ get: () => revision,
10
+ subscribe(notify) {
11
+ listeners.add(notify);
12
+ return () => {
13
+ listeners.delete(notify);
14
+ };
15
+ }
16
+ };
17
+ /**
18
+ * Register trusted, bundled Iconify JSON. Replaces the collection with the same
19
+ * prefix and updates mounted Icons. No fetching or default set is provided.
20
+ * Treat the supplied JSON as immutable after registration.
21
+ * @throws TypeError if the prefix or icons dictionary is missing/invalid.
22
+ */
23
+ function addCollection(collection) {
24
+ if (!collection || typeof collection.prefix !== "string" || !identifier.test(collection.prefix) || !collection.icons || typeof collection.icons !== "object" || Array.isArray(collection.icons)) throw new TypeError("Expected Iconify JSON with a non-empty prefix and icons dictionary");
25
+ collections.set(collection.prefix, {
26
+ ...collection,
27
+ icons: Object.assign(Object.create(null), collection.icons),
28
+ aliases: Object.assign(Object.create(null), collection.aliases)
29
+ });
30
+ cache.clear();
31
+ revision++;
32
+ for (const notify of [...listeners]) notify();
33
+ }
34
+ /** Resolve `prefix:name`, including aliases and collection dimensions; missing/invalid names return undefined. */
35
+ function resolveIcon(name) {
36
+ if (cache.has(name)) return cache.get(name);
37
+ const parts = name.split(":");
38
+ if (parts.length !== 2 || !parts.every((part) => identifier.test(part))) return;
39
+ const collection = collections.get(parts[0]);
40
+ const icon = collection ? getIconData(collection, parts[1]) ?? void 0 : void 0;
41
+ cache.set(name, icon);
42
+ return icon;
43
+ }
44
+ //#endregion
45
+ export { addCollection, registry, resolveIcon };
@@ -0,0 +1,37 @@
1
+ import { iconToSVG } from "@iconify/utils/lib/svg/build";
2
+ import { replaceIDs } from "@iconify/utils/lib/svg/id";
3
+ //#region src/svg.ts
4
+ function escapeAttribute(value) {
5
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
6
+ }
7
+ /**
8
+ * Convert trusted icon data to SVG without DOM APIs. Applies transforms, keeps
9
+ * multicolor fills, and gives definitions unique IDs on every invocation.
10
+ * Without a color, markup uses currentColor; native callers should supply a
11
+ * concrete tint. This is a converter, not an SVG sanitizer.
12
+ * @throws RangeError if size is not finite and positive.
13
+ */
14
+ function iconToSvg(icon, options = {}) {
15
+ const size = options.size ?? 24;
16
+ if (!Number.isFinite(size) || size <= 0) throw new RangeError("Icon size must be finite and positive");
17
+ const result = iconToSVG(icon, {
18
+ height: size,
19
+ rotate: options.rotate ?? 0,
20
+ hFlip: options.hFlip ?? false,
21
+ vFlip: options.vFlip ?? false
22
+ });
23
+ const color = escapeAttribute(options.color ?? "currentColor");
24
+ const body = replaceIDs(result.body).replace(/currentColor/g, () => color);
25
+ const viewBox = result.attributes.viewBox;
26
+ const width = Number(result.attributes.width);
27
+ const height = Number(result.attributes.height);
28
+ return {
29
+ body,
30
+ viewBox,
31
+ width,
32
+ height,
33
+ markup: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" width="${width}" height="${height}" color="${color}" fill="${color}">${body}</svg>`
34
+ };
35
+ }
36
+ //#endregion
37
+ export { iconToSvg };
package/package.json CHANGED
@@ -1,11 +1,91 @@
1
1
  {
2
2
  "name": "@octane-xplat/icons",
3
- "version": "0.0.1",
4
- "description": "Placeholder stub for @octane-xplat/icons — no supported API yet.",
3
+ "version": "0.10.0",
4
+ "description": "Bundled, set-agnostic Iconify icons for Octane web and NativeScript",
5
+ "keywords": [
6
+ "cross-platform",
7
+ "iconify",
8
+ "icons",
9
+ "nativescript",
10
+ "octane"
11
+ ],
12
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/icons",
5
13
  "license": "MIT",
6
14
  "repository": {
7
15
  "type": "git",
8
16
  "url": "https://github.com/octane-xplat/octane-xplat",
9
17
  "directory": "packages/icons"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "types/generated"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "macos": {
27
+ "types": "./types/generated/index.macos.d.ts",
28
+ "default": "./dist/macos/index.js"
29
+ },
30
+ "web": {
31
+ "types": "./types/generated/index.web.d.ts",
32
+ "default": "./dist/web/index.js"
33
+ },
34
+ "native": {
35
+ "types": "./types/generated/index.d.ts",
36
+ "default": "./dist/native/index.js"
37
+ },
38
+ "types": "./types/generated/index.d.ts",
39
+ "default": "./dist/native/index.js"
40
+ }
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@iconify/types": "2.0.0",
47
+ "@iconify/utils": "3.1.7",
48
+ "@octane-xplat/ui": "0.10.0"
49
+ },
50
+ "devDependencies": {
51
+ "@nativescript-community/octane": "0.2.4",
52
+ "@nativescript/core": "9.1.2",
53
+ "@octane-xplat/macos-renderer": "0.10.0",
54
+ "@octanejs/vite-plugin": "0.1.61",
55
+ "@tsrx/typescript-plugin": "0.4.11",
56
+ "octane": "0.6.3",
57
+ "tsrx-typegen": "0.2.2",
58
+ "typescript": "5.9.3",
59
+ "vite": "8.3.0"
60
+ },
61
+ "peerDependencies": {
62
+ "@nativescript-community/octane": ">=0.2.1 <1",
63
+ "@nativescript/core": ">=9.1.0 <10",
64
+ "octane": ">=0.6.3 <1"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@nativescript-community/octane": {
68
+ "optional": true
69
+ },
70
+ "@nativescript/core": {
71
+ "optional": true
72
+ },
73
+ "octane": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "nativescript": {
78
+ "platforms": {
79
+ "android": "9.1.0",
80
+ "ios": "9.1.0"
81
+ }
82
+ },
83
+ "scripts": {
84
+ "build": "vite build && vite build --mode native && vite build --mode macos && pnpm typegen && node ../../scripts/check-native-dist.mjs",
85
+ "typegen": "tsrx-typegen --target icons",
86
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.web.json && tsrx-tsc --noEmit -p tsconfig.native.json && tsrx-tsc --noEmit -p tsconfig.macos.json",
87
+ "test": "pnpm build && node --test tests/*.test.mjs",
88
+ "pack:check": "tsrx-typegen --pack-check",
89
+ "test:packed": "node tests/packed-consumer.mjs"
10
90
  }
11
- }
91
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "files": [
4
+ "Icon.d.ts",
5
+ "Icon.macos.d.ts",
6
+ "Icon.web.d.ts",
7
+ "index.d.ts",
8
+ "index.macos.d.ts",
9
+ "index.web.d.ts",
10
+ "props.d.ts",
11
+ "registry.d.ts",
12
+ "svg.d.ts"
13
+ ]
14
+ }
@@ -0,0 +1,3 @@
1
+ import type { IconProps } from './props.js';
2
+ /** Render a bundled icon through ui's vendored SVGView; missing names render nothing. */
3
+ export declare function Icon(props: IconProps): unknown;
@@ -0,0 +1,3 @@
1
+ import type { IconProps } from './props.js';
2
+ /** AppKit's renderer aliases the universal renderer entry and decodes SVG with NSImage. */
3
+ export declare function Icon(props: IconProps): unknown;
@@ -0,0 +1,3 @@
1
+ import type { IconProps } from './props.js';
2
+ /** Render a bundled icon as inline SVG; missing names render nothing. */
3
+ export declare function Icon(props: IconProps): unknown;
@@ -0,0 +1,5 @@
1
+ export { Icon } from './Icon.js';
2
+ export { addCollection, resolveIcon } from './registry.js';
3
+ export { iconToSvg } from './svg.js';
4
+ export type { IconProps, IconOptions, IconSvg } from './props.js';
5
+ export type { IconifyIcon, IconifyJSON } from '@iconify/types';
@@ -0,0 +1,5 @@
1
+ export { Icon } from './Icon.macos.js';
2
+ export { addCollection, resolveIcon } from './registry.js';
3
+ export { iconToSvg } from './svg.js';
4
+ export type { IconProps, IconOptions, IconSvg } from './props.js';
5
+ export type { IconifyIcon, IconifyJSON } from '@iconify/types';
@@ -0,0 +1,5 @@
1
+ export { Icon } from './Icon.web.js';
2
+ export { addCollection, resolveIcon } from './registry.js';
3
+ export { iconToSvg } from './svg.js';
4
+ export type { IconProps, IconOptions, IconSvg } from './props.js';
5
+ export type { IconifyIcon, IconifyJSON } from '@iconify/types';
@@ -0,0 +1,25 @@
1
+ import type { IconifyTransformations } from '@iconify/types';
2
+ /** SVG sizing, tint, and transforms shared by every target. */
3
+ export interface IconOptions extends IconifyTransformations {
4
+ /** Height in logical pixels; width follows the transformed aspect ratio. Default: 24. */
5
+ size?: number;
6
+ /** Monochrome tint. Explicit fills in multicolor icons remain intact. */
7
+ color?: string;
8
+ }
9
+ /** Props for a bundled Iconify icon. */
10
+ export interface IconProps extends IconOptions {
11
+ /** Registered collection prefix and icon name, such as `heroicons:arrow-right`. */
12
+ name: string;
13
+ id?: string;
14
+ className?: string;
15
+ /** Accessible description. Omit for a decorative icon. */
16
+ label?: string;
17
+ }
18
+ /** DOM-free SVG output, ready for an inline SVG or native SVGView. */
19
+ export interface IconSvg {
20
+ body: string;
21
+ viewBox: string;
22
+ width: number;
23
+ height: number;
24
+ markup: string;
25
+ }
@@ -0,0 +1,14 @@
1
+ import type { IconifyIcon, IconifyJSON } from '@iconify/types';
2
+ export declare const registry: {
3
+ get: () => number;
4
+ subscribe(notify: () => void): () => void;
5
+ };
6
+ /**
7
+ * Register trusted, bundled Iconify JSON. Replaces the collection with the same
8
+ * prefix and updates mounted Icons. No fetching or default set is provided.
9
+ * Treat the supplied JSON as immutable after registration.
10
+ * @throws TypeError if the prefix or icons dictionary is missing/invalid.
11
+ */
12
+ export declare function addCollection(collection: IconifyJSON): void;
13
+ /** Resolve `prefix:name`, including aliases and collection dimensions; missing/invalid names return undefined. */
14
+ export declare function resolveIcon(name: string): IconifyIcon | undefined;
@@ -0,0 +1,10 @@
1
+ import type { IconifyIcon } from '@iconify/types';
2
+ import type { IconOptions, IconSvg } from './props.js';
3
+ /**
4
+ * Convert trusted icon data to SVG without DOM APIs. Applies transforms, keeps
5
+ * multicolor fills, and gives definitions unique IDs on every invocation.
6
+ * Without a color, markup uses currentColor; native callers should supply a
7
+ * concrete tint. This is a converter, not an SVG sanitizer.
8
+ * @throws RangeError if size is not finite and positive.
9
+ */
10
+ export declare function iconToSvg(icon: IconifyIcon, options?: IconOptions): IconSvg;