@versini/ui-icon-renderer 1.0.4 → 2.1.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/README.md +140 -0
- package/dist/184.js +28 -0
- package/dist/475.js +55 -0
- package/dist/data/icon-attachment-light.js +8 -0
- package/dist/data/icon-attachment.js +8 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -37
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1 +1,141 @@
|
|
|
1
1
|
# @versini/ui-icon-renderer
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@versini/ui-icon-renderer)
|
|
4
|
+
>)
|
|
5
|
+
|
|
6
|
+
> A React hook for lazy-loading icons from `@versini/ui-icons` on demand.
|
|
7
|
+
|
|
8
|
+
The Icon Renderer package provides `useIconRenderer`, a hook that dynamically imports icons by name at runtime. This avoids bundling all 100+ icons upfront, making it ideal for components like icon pickers where the full icon set is needed but shouldn't impact initial bundle size.
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Usage](#usage)
|
|
15
|
+
- [API](#api)
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **🚀 Lazy Loading**: Icons are loaded on demand via dynamic imports — no upfront bundle cost
|
|
20
|
+
- **⚡ Optional Preloading**: Warm the cache ahead of time to prevent flash on render
|
|
21
|
+
- **🧩 Simple Hook API**: Single `useIconRenderer` hook covers all use cases
|
|
22
|
+
- **🌲 Tree-shakeable**: Only the hook is included in your bundle, icon data stays lazy
|
|
23
|
+
- **🔧 TypeScript**: Fully typed with comprehensive prop definitions
|
|
24
|
+
- **📦 Shared Cache**: Module-level cache ensures each icon is loaded only once across components
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @versini/ui-icon-renderer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Note**: This package requires `@versini/ui-icons` as a peer dependency.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Render Icons Without Preloading
|
|
37
|
+
|
|
38
|
+
When you don't know which icon will be needed ahead of time, or preloading isn't necessary. The icon loads automatically when rendered (brief null while loading).
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useIconRenderer } from "@versini/ui-icon-renderer";
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
const [IconRenderer] = useIconRenderer();
|
|
45
|
+
const [selectedIcon, setSelectedIcon] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<button onClick={() => setSelectedIcon("IconAdd")}>Show Icon</button>
|
|
50
|
+
{selectedIcon && IconRenderer && <IconRenderer icon={selectedIcon} />}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Preload Icons to Prevent Flash
|
|
57
|
+
|
|
58
|
+
When you know which icons will be needed, preload them before rendering to avoid a flash of empty content.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { useIconRenderer } from "@versini/ui-icon-renderer";
|
|
62
|
+
|
|
63
|
+
function App() {
|
|
64
|
+
const [IconRenderer, preloadIcons] = useIconRenderer();
|
|
65
|
+
const [showIcons, setShowIcons] = useState(false);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<button
|
|
70
|
+
onMouseEnter={() => preloadIcons(["IconAdd", "IconDelete", "IconEdit"])}
|
|
71
|
+
onClick={() => setShowIcons(true)}
|
|
72
|
+
>
|
|
73
|
+
Show Icons
|
|
74
|
+
</button>
|
|
75
|
+
{showIcons && IconRenderer && (
|
|
76
|
+
<div>
|
|
77
|
+
<IconRenderer icon="IconAdd" />
|
|
78
|
+
<IconRenderer icon="IconDelete" />
|
|
79
|
+
<IconRenderer icon="IconEdit" />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Icon Picker
|
|
88
|
+
|
|
89
|
+
Load all available icons lazily for an icon picker component.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useIconRenderer } from "@versini/ui-icon-renderer";
|
|
93
|
+
|
|
94
|
+
const ALL_ICON_NAMES = ["IconAdd", "IconDelete", "IconEdit", "IconSearch"];
|
|
95
|
+
|
|
96
|
+
function IconPicker({ onSelect }: { onSelect: (name: string) => void }) {
|
|
97
|
+
const [IconRenderer, preloadIcons] = useIconRenderer();
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
preloadIcons(ALL_ICON_NAMES);
|
|
101
|
+
}, [preloadIcons]);
|
|
102
|
+
|
|
103
|
+
if (!IconRenderer) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="grid grid-cols-6 gap-2">
|
|
109
|
+
{ALL_ICON_NAMES.map((name) => (
|
|
110
|
+
<button key={name} onClick={() => onSelect(name)}>
|
|
111
|
+
<IconRenderer icon={name} />
|
|
112
|
+
</button>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### useIconRenderer
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const [IconRenderer, preloadIcons] = useIconRenderer();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Returns a tuple:
|
|
128
|
+
|
|
129
|
+
| Index | Name | Type | Description |
|
|
130
|
+
| ----- | ------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
131
|
+
| 0 | IconRenderer | `React.FC<IconRendererProps> \| null` | The icon component, or `null` while the module is loading. Available after first render cycle. |
|
|
132
|
+
| 1 | preloadIcons | `(icons: string[]) => Promise<void>` | Preloads icon data into the cache. Optional — icons auto-load when rendered if not preloaded. |
|
|
133
|
+
|
|
134
|
+
### IconRenderer Props
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Default | Description |
|
|
137
|
+
| --------- | -------- | ------- | ---------------------------------------------------------------------- |
|
|
138
|
+
| icon | `string` | - | The name of the icon to render (e.g. `"IconAdd"`, `"IconDeleteLight"`) |
|
|
139
|
+
| className | `string` | - | CSS class(es) to add to the SVG element |
|
|
140
|
+
|
|
141
|
+
_Also accepts all standard SVG element props (`React.ComponentPropsWithoutRef<"svg">`)._
|
package/dist/184.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function useIconRenderer() {
|
|
6
|
+
const modRef = useRef(null);
|
|
7
|
+
const [mod, setMod] = useState(null);
|
|
8
|
+
useEffect(()=>{
|
|
9
|
+
/* v8 ignore start -- dynamic imports are not fully instrumented by V8 */ import("./475.js").then((mod) => ({ IconRenderer: mod.IconRenderer,preloadIcon: mod.preloadIcon,preloadIcons: mod.preloadIcons })).then((m)=>{
|
|
10
|
+
modRef.current = m;
|
|
11
|
+
setMod(m);
|
|
12
|
+
}).catch(()=>{});
|
|
13
|
+
/* v8 ignore stop */ }, []);
|
|
14
|
+
/* v8 ignore start -- dynamic imports are not fully instrumented by V8 */ const preload = useCallback((icons)=>{
|
|
15
|
+
const ensure = modRef.current ? Promise.resolve(modRef.current) : import("./475.js").then((mod) => ({ IconRenderer: mod.IconRenderer,preloadIcon: mod.preloadIcon,preloadIcons: mod.preloadIcons })).then((m)=>{
|
|
16
|
+
modRef.current = m;
|
|
17
|
+
setMod(m);
|
|
18
|
+
return m;
|
|
19
|
+
});
|
|
20
|
+
return ensure.then((m)=>m.preloadIcons(icons)).catch(()=>{});
|
|
21
|
+
}, []);
|
|
22
|
+
/* v8 ignore stop */ return [
|
|
23
|
+
mod?.IconRenderer ?? null,
|
|
24
|
+
preload
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { useEffect, useIconRenderer, useState };
|
package/dist/475.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "./184.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const iconCache = new Map();
|
|
8
|
+
const toKebabCase = (name)=>name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
9
|
+
const preloadIcon = async (icon)=>{
|
|
10
|
+
if (iconCache.has(icon)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
/* v8 ignore start -- dynamic template-literal imports are not instrumented by V8 */ try {
|
|
14
|
+
const kebabIcon = toKebabCase(icon);
|
|
15
|
+
const mod = await import(`./data/${kebabIcon}.js`);
|
|
16
|
+
if (mod?.default) {
|
|
17
|
+
iconCache.set(icon, mod.default);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Icon not found — skip silently
|
|
21
|
+
}
|
|
22
|
+
/* v8 ignore stop */ };
|
|
23
|
+
const preloadIcons = (icons)=>Promise.all(icons.map(preloadIcon));
|
|
24
|
+
const IconRenderer = ({ icon, ...rest })=>{
|
|
25
|
+
const [IconComponent, setIconComponent] = useState(()=>iconCache.get(icon) ?? null);
|
|
26
|
+
useEffect(()=>{
|
|
27
|
+
if (iconCache.has(icon)) {
|
|
28
|
+
setIconComponent(()=>iconCache.get(icon));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
setIconComponent(null);
|
|
32
|
+
/* v8 ignore start -- dynamic template-literal imports are not instrumented by V8 */ (async ()=>{
|
|
33
|
+
try {
|
|
34
|
+
await preloadIcon(icon);
|
|
35
|
+
const cached = iconCache.get(icon);
|
|
36
|
+
if (cached) {
|
|
37
|
+
setIconComponent(()=>cached);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Icon not found
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
/* v8 ignore stop */ }, [
|
|
44
|
+
icon
|
|
45
|
+
]);
|
|
46
|
+
if (!IconComponent) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return /*#__PURE__*/ jsx(IconComponent, {
|
|
50
|
+
"data-slot": "icon",
|
|
51
|
+
...rest
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { IconRenderer, preloadIcon, preloadIcons };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { JSX } from 'react/jsx-runtime';
|
|
2
|
-
import { default as React_2 } from 'react';
|
|
3
2
|
|
|
4
|
-
export declare
|
|
3
|
+
export declare function useIconRenderer(): readonly [(({ icon, ...rest }: {
|
|
5
4
|
icon: string;
|
|
6
|
-
} &
|
|
5
|
+
} & React.ComponentPropsWithoutRef<"svg">) => JSX.Element | null) | null, (icons: string[]) => Promise<void | void[]>];
|
|
7
6
|
|
|
8
7
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,2 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
1
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const toKebabCase = (name)=>name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
9
|
-
const IconRenderer = ({ icon, ...rest })=>{
|
|
10
|
-
const [IconComponent, setIconComponent] = useState(null);
|
|
11
|
-
useEffect(()=>{
|
|
12
|
-
setIconComponent(null);
|
|
13
|
-
/* v8 ignore start -- dynamic template-literal imports are not instrumented by V8 */ (async ()=>{
|
|
14
|
-
try {
|
|
15
|
-
const kebabIcon = toKebabCase(icon);
|
|
16
|
-
const mod = await import(`./data/${kebabIcon}.js`);
|
|
17
|
-
if (mod && mod.default) {
|
|
18
|
-
setIconComponent(()=>mod.default);
|
|
19
|
-
}
|
|
20
|
-
} catch {
|
|
21
|
-
// Icon not found
|
|
22
|
-
}
|
|
23
|
-
})();
|
|
24
|
-
/* v8 ignore stop */ }, [
|
|
25
|
-
icon
|
|
26
|
-
]);
|
|
27
|
-
if (!IconComponent) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
return /*#__PURE__*/ jsx(IconComponent, {
|
|
31
|
-
"data-slot": "icon",
|
|
32
|
-
...rest
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/* v8 ignore next 1 */
|
|
37
|
-
|
|
38
|
-
export { IconRenderer };
|
|
2
|
+
export { useIconRenderer } from "./184.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-icon-renderer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
17
23
|
"files": [
|
|
18
24
|
"dist"
|
|
19
25
|
],
|
|
@@ -37,11 +43,11 @@
|
|
|
37
43
|
"test": "vitest run"
|
|
38
44
|
},
|
|
39
45
|
"peerDependencies": {
|
|
40
|
-
"@versini/ui-icons": "4.
|
|
46
|
+
"@versini/ui-icons": "4.23.0",
|
|
41
47
|
"clsx": ">=2"
|
|
42
48
|
},
|
|
43
49
|
"sideEffects": [
|
|
44
50
|
"**/*.css"
|
|
45
51
|
],
|
|
46
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "0ccbca8586510e8619f039e193b68ef15ab795c5"
|
|
47
53
|
}
|