@versini/ui-icon-renderer 1.0.4 → 2.0.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 CHANGED
@@ -1 +1,141 @@
1
1
  # @versini/ui-icon-renderer
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@versini/ui-icon-renderer?style=flat-square)](https://www.npmjs.com/package/@versini/ui-icon-renderer)
4
+ ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-icon-renderer?style=flat-square&label=size%20(gzip)>)
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 const IconRenderer: ({ icon, ...rest }: {
3
+ export declare function useIconRenderer(): readonly [(({ icon, ...rest }: {
5
4
  icon: string;
6
- } & React_2.ComponentPropsWithoutRef<"svg">) => JSX.Element | null;
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.4",
3
+ "version": "2.0.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
  ],
@@ -43,5 +49,5 @@
43
49
  "sideEffects": [
44
50
  "**/*.css"
45
51
  ],
46
- "gitHead": "85c2706ce9e267f57683ecb346a86b6f5b0cd6f3"
52
+ "gitHead": "2396e2cb24d3ced398f6d4eabb7ce6cd10680dcf"
47
53
  }