Skip to content

Commit eaf5d94

Browse files
committed
v0.1.5
1 parent 570ff89 commit eaf5d94

File tree

12 files changed

+178
-140
lines changed

12 files changed

+178
-140
lines changed

README.md

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# @react-zero-ui/icon-sprite
44

5-
[![MIT](https://img.shields.io/badge/License-MIT-green.svg)](#) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](#)
5+
[![MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/react-zero-ui/icon-sprite/blob/main/LICENSE) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](https://www.npmjs.com/package/@react-zero-ui/icon-sprite)
66

77

88
</div>
@@ -15,18 +15,18 @@
1515
1616
---
1717

18-
### 📊 Live Demo – 270% Smaller HTML Payload
18+
**📊 Live Demo - ~270% Smaller HTML**
19+
**See the difference:** [View Demo](https://zero-ui.dev/icon-sprite)
20+
1921

20-
**See the size difference in action:** [View Demo](https://zero-ui.dev/icon-sprite)
21-
270% smaller HTML output compared to directly inlining Lucide React components.
2222

2323
---
2424

2525

2626
## 🧠 What This Library Does
2727

2828
1. **Full Lucide-React DX in development**
29-
Easy imports, hot reload, JSX props import icons from `@react-zero-ui/icon-sprite` instead of `lucide-react`.
29+
Easy imports, hot reload, JSX props - import icons from `@react-zero-ui/icon-sprite` instead of `lucide-react`.
3030

3131
2. **Zero-runtime in production**
3232
Every icon becomes `<use href="/icons.svg#id" />` at build time.
@@ -35,19 +35,22 @@
3535
Only icons actually used in your app are included.
3636

3737
## 🙏 Custom Icon Support
38-
Drop SVGs into **`/zero-ui-icons/`** at your project root, then use `<CustomIcon />` with the `name` prop set to the file name (without `.svg`).
38+
Drop SVGs into **`/public/zero-ui-icons/`**, then use `<CustomIcon />` with the filename (no `.svg`).
3939

4040
> [!TIP]
4141
>```txt
42-
>📁/zero-ui-icons/
43-
> └── dog.svg
42+
>📁/public
43+
> └──📁/zero-ui-icons/
44+
> └──dog.svg
4445
> ```
4546
> ```tsx
4647
>import { CustomIcon } from "@react-zero-ui/icon-sprite";
4748
>//❗The name MUST match the name of the file name (no .svg extension).
4849
><CustomIcon name="dog" size={24} />
4950
>```
5051
52+
> [!INFO]
53+
> In dev you may see a brief FOUC using custom icons; this is removed in production.
5154
5255
---
5356
@@ -77,7 +80,7 @@ Or add this to your `package.json` scripts:
7780
}
7881
}
7982
```
80-
That's it!
83+
That's it! You can now use the icons in your app.
8184

8285
---
8386

@@ -93,20 +96,23 @@ That's it!
9396
import { ArrowRight, Mail } from "@react-zero-ui/icon-sprite";
9497

9598
<ArrowRight size={24} className="text-gray-600" />
99+
<Mail width={24} height={24} />
96100
```
97101

98-
### For Custom Icons:
102+
### Custom Icons:
99103

104+
Drop SVGs into **`/public/zero-ui-icons/`**, then use `<CustomIcon />` with the filename (no `.svg`).
100105
```tsx
101106
import { CustomIcon } from "@react-zero-ui/icon-sprite";
102-
107+
//❗The name MUST match the name of the file name (without .svg).
103108
<CustomIcon name="dog" size={32} />
104109
```
110+
105111
---
106112

107113
## 🧪 How It Works (Under the Hood)
108114

109-
### ✅ Development Mode: DX First
115+
### ✅ Development: DX First
110116

111117
In dev, each icon wrapper looks like this:
112118

@@ -158,30 +164,18 @@ This runs the full pipeline:
158164

159165

160166
---
161-
## ✨ Why This Is Better
162167

163-
* **DX-first**: No flicker. No sprite caching. Live updates.
164-
* **Runtime-free in production**: Sprites are native, fast, lightweight & highly Cached.
165-
* **Only ships the icons you actually use** — smallest possible sprite.
166-
* **Minimal install**: No runtime dependency tree. Just React + Lucide.
168+
## ✨ Why This Beats Icon Libraries Everywhere
167169

170+
* **DX-first in dev**: No flicker. No sprite caching. Live updates.
171+
* **Zero-runtime in production**: Sprites are native, fast, lightweight & highly Cached.
172+
* **Only ships the icons you actually use** - smallest possible sprite.
173+
* **Custom icon support**: Drop SVGs into `/public/zero-ui-icons/` and use `<CustomIcon />`
168174

169-
---
170-
171-
## 🧠 Final Thoughts
172175

173-
This is one of the **most optimized** icon systems for serious frontends:
176+
Made with ❤️ for the React community by [@austin1serb](https://github.com/austin1serb)
174177

175-
* First-class developer experience (DX) with Lucide's React components.
176-
* Production builds with **zero JavaScript**, just SVGs.
177-
* Custom icon support out-of-the-box.
178-
* Strict static analysis = smallest possible bundle.
179-
* Fully compatible with Next.js 15+, Vite, or any modern React stack.
180-
181-
---
182178

183-
184-
185179
<!--
186180
📂 icon-sprite/
187181
├── 📂 node_modules

icon-sprite/README.md

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
# @react-zero-ui/icon-sprite
44

5-
[![MIT](https://img.shields.io/badge/License-MIT-green.svg)](#) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](#)
5+
[![MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/react-zero-ui/icon-sprite/blob/main/LICENSE) [![npm](https://img.shields.io/npm/v/@react-zero-ui/icon-sprite.svg)](https://www.npmjs.com/package/@react-zero-ui/icon-sprite)
6+
67

78

89
</div>
@@ -14,12 +15,17 @@
1415
> Part of the [React Zero-UI](https://github.com/react-zero-ui) ecosystem.
1516
1617

18+
---
19+
20+
**📊 Live Demo — ~270% Smaller HTML**
21+
**See the difference:** [View Demo](https://zero-ui.dev/icon-sprite)
22+
1723
---
1824

1925
## 🧠 What This Library Does
2026

2127
1. **Full Lucide-React DX in development**
22-
Easy imports, hot reload, JSX props — import icons from `@react-zero-ui/icon-sprite` instead of `lucide-react`.
28+
Import from `@react-zero-ui/icon-sprite` instead of `lucide-react`. Hot reload, JSX props, no caching headaches.
2329

2430
2. **Zero-runtime in production**
2531
Every icon becomes `<use href="/icons.svg#id" />` at build time.
@@ -28,12 +34,13 @@
2834
Only icons actually used in your app are included.
2935

3036
## 🙏 Custom Icon Support
31-
Drop SVGs into **`/zero-ui-icons/`** at your project root, then use `<CustomIcon />` with the `name` prop set to the file name (without `.svg`).
37+
Drop SVGs into **`/public/zero-ui-icons/`**, then use `<CustomIcon />` with the filename (no `.svg`).
3238

33-
> ![Tip](https://img.shields.io/badge/Tip-green)
39+
>![Tip](https://img.shields.io/badge/Tip-green)
3440
>```txt
35-
>📁/zero-ui-icons/
36-
> └── dog.svg
41+
>📁/public
42+
> └──📁/zero-ui-icons/
43+
> └──dog.svg
3744
> ```
3845
> ```tsx
3946
>import { CustomIcon } from "@react-zero-ui/icon-sprite";
@@ -42,6 +49,11 @@ Drop SVGs into **`/zero-ui-icons/`** at your project root, then use `<CustomIcon
4249
>```
4350
4451
52+
>![Info](https://img.shields.io/badge/Info-blue)
53+
> In dev you may see a brief FOUC using custom icons; this is removed in production.
54+
55+
56+
4557
---
4658
4759
## 📦 Installation
@@ -86,15 +98,20 @@ That's it!
8698
import { ArrowRight, Mail } from "@react-zero-ui/icon-sprite";
8799

88100
<ArrowRight size={24} className="text-gray-600" />
101+
<Mail width={24} height={24} />
89102
```
90103

91-
### For Custom Icons:
104+
### Custom Icons:
92105

106+
Drop SVGs into **`/public/zero-ui-icons/`**, then use `<CustomIcon />` with the filename (no `.svg`).
93107
```tsx
94108
import { CustomIcon } from "@react-zero-ui/icon-sprite";
95-
109+
//❗The name MUST match the name of the file name (without .svg).
96110
<CustomIcon name="dog" size={32} />
97111
```
112+
113+
114+
98115
---
99116

100117
## 🧪 How It Works (Under the Hood)
@@ -151,25 +168,14 @@ This runs the full pipeline:
151168

152169

153170
---
154-
## ✨ Why This Is Better
155171

156-
* **DX-first**: No flicker. No sprite caching. Live updates.
157-
* **Runtime-free in production**: Sprites are native, fast, lightweight & highly Cached.
158-
* **Only ships the icons you actually use** — smallest possible sprite.
159-
* **Minimal install**: No runtime dependency tree. Just React + Lucide.
172+
## ✨ Why This Beats Icon Libraries Everywhere
160173

174+
* **DX-first in dev**: No flicker. No sprite caching. Live updates.
175+
* **Zero-runtime in production**: Sprites are native, fast, lightweight & highly Cached.
176+
* **Only ships the icons you actually use** - smallest possible sprite.
177+
* **Custom icon support**: Drop SVGs into `/public/zero-ui-icons/` and use `<CustomIcon />`
161178

162-
---
163179

164-
## 🧠 Final Thoughts
180+
Made with ❤️ for the React community by [@austin1serb](https://github.com/austin1serb)
165181

166-
This is one of the **most optimized** icon systems for serious frontends:
167-
168-
* First-class developer experience (DX) with Lucide's React components.
169-
* Production builds with **zero JavaScript**, just SVGs.
170-
* Custom icon support out-of-the-box.
171-
* Strict static analysis = smallest possible bundle.
172-
* Fully compatible with Next.js 15+, Vite, or any modern React stack.
173-
174-
---
175-

icon-sprite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "@react-zero-ui/icon-sprite",
3+
"version": "0.1.5",
34
"description": "Generate a single SVG sprite containing only the icons you used (Lucide + custom). Lucid to SVG sprite solution for React.",
45
"keywords": [
56
"react",
@@ -23,7 +24,6 @@
2324
"sideEffects": false,
2425
"license": "MIT",
2526
"private": false,
26-
"version": "0.1.4",
2727
"type": "module",
2828
"main": "dist/index.js",
2929
"module": "dist/index.js",

icon-sprite/scripts/build-sprite.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ for (const file of fs.readdirSync(iconsDir)) {
3535
}
3636

3737
// 4️⃣ Optionally include *all* SVGs from your custom folder
38-
const customDir = path.resolve(process.cwd(), CUSTOM_SVG_DIR);
38+
const customDir = path.resolve(process.cwd(), "public", CUSTOM_SVG_DIR);
3939
if (fs.existsSync(customDir)) {
4040
for (const file of fs.readdirSync(customDir)) {
4141
if (!file.endsWith(".svg")) continue;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"use client";
2+
import { useEffect, useState, useLayoutEffect, useRef } from "react";
3+
import { CUSTOM_SVG_DIR, SPRITE_PATH } from "./config";
4+
import { renderUse } from "./_shared";
5+
import { IconProps } from "./icons/CustomIcon";
6+
7+
type Payload = { attrs: Record<string, string>; innerHTML: string };
8+
const mem = new Map<string, Payload>();
9+
10+
function extractSVGContent(svg: string): Payload {
11+
const div = document.createElement("div");
12+
div.innerHTML = svg.trim();
13+
const svgEl = div.querySelector("svg");
14+
if (!svgEl) throw new Error("Bad SVG: <svg> not found");
15+
// sanitize
16+
div.querySelectorAll("script").forEach((n) => n.remove());
17+
svgEl.querySelectorAll("*").forEach((el) => {
18+
for (const { name } of Array.from(el.attributes)) if (/^on[a-z]+/i.test(name)) el.removeAttribute(name);
19+
});
20+
const attrs: Record<string, string> = {};
21+
for (const a of Array.from(svgEl.attributes)) if (!/^(width|height)$/i.test(a.name)) attrs[a.name] = a.value;
22+
return { attrs, innerHTML: svgEl.innerHTML.trim() };
23+
}
24+
25+
function DevCustomIcon({ name, size, height, width, ...rest }: IconProps) {
26+
const [payload, setPayload] = useState<Payload | null>(() => (typeof window !== "undefined" ? mem.get(name) ?? null : null));
27+
const svgRef = useRef<SVGSVGElement>(null);
28+
29+
// After mount, stamp raw attributes so React doesn't validate/camelCase them.
30+
useLayoutEffect(() => {
31+
const el = svgRef.current;
32+
if (!el || !payload) return;
33+
// Merge class from payload with incoming className, if any
34+
const incomingClass = (rest as any).className ?? "";
35+
let payloadClass = payload.attrs.class ?? payload.attrs.className ?? "";
36+
const mergedClass = [incomingClass, payloadClass].filter(Boolean).join(" ").trim();
37+
if (mergedClass) el.setAttribute("class", mergedClass);
38+
// Apply all other attrs (skip class handled above and width/height)
39+
for (const [k, v] of Object.entries(payload.attrs)) {
40+
if (k === "class" || k === "className") continue;
41+
if (/^(width|height)$/i.test(k)) continue;
42+
if (k === "xmlns" || k.startsWith("xmlns:")) continue;
43+
el.setAttribute(k, v);
44+
}
45+
}, [payload, rest]);
46+
47+
useEffect(() => {
48+
if (typeof window === "undefined") return;
49+
const ctrl = new AbortController();
50+
// sanitize for leading/trailing slashes
51+
const base = "/" + String(CUSTOM_SVG_DIR).replace(/^\/+|\/+$/g, "");
52+
const url = `${base}/${encodeURIComponent(name)}.svg?v=${Date.now()}`;
53+
fetch(url, { cache: "no-store", signal: ctrl.signal })
54+
.then((r) => (r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))))
55+
.then((txt) => {
56+
if (ctrl.signal.aborted) return;
57+
const p = extractSVGContent(txt);
58+
mem.set(name, p);
59+
setPayload(p);
60+
})
61+
.catch((err) => {
62+
if (err.name !== "AbortError") console.warn(`[CustomIcon] failed to load ${url}:`, err);
63+
if (!ctrl.signal.aborted) setPayload(null);
64+
});
65+
return () => ctrl.abort();
66+
}, [name]);
67+
68+
if (payload) {
69+
return (
70+
<svg
71+
ref={svgRef}
72+
{...(size != null ? { width: size, height: size } : {})}
73+
{...(height != null ? { height } : {})}
74+
{...(width != null ? { width } : {})}
75+
{...rest}
76+
dangerouslySetInnerHTML={{ __html: payload.innerHTML }}
77+
/>
78+
);
79+
}
80+
81+
// fallback if fetch fails in dev
82+
return renderUse(name, width, height, size, SPRITE_PATH, rest);
83+
}
84+
85+
export default DevCustomIcon;

0 commit comments

Comments
 (0)