@rdicons/core 0.0.0-stage → 0.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/LICENSE +25 -0
- package/README.md +22 -2
- package/dist/browser.cjs +214 -0
- package/dist/browser.d.cts +34 -0
- package/dist/browser.d.ts +34 -0
- package/dist/browser.js +171 -0
- package/dist/index.cjs +273 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +229 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rdicons contributors
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The icon sets bundled at build time are third-party works under their own
|
|
24
|
+
licenses (see THIRD_PARTY_NOTICES.md). The MIT license above covers this
|
|
25
|
+
repository's source code only.
|
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rdicons/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The small, framework-free engine behind [rdicons](https://rdicons.vimalbhatt.com): recolor an icon, change its stroke and size, add padding or a background, convert SVG to a React component, and export PNG.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @rdicons/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { buildSvg, svgToJsx } from '@rdicons/core'
|
|
11
|
+
|
|
12
|
+
const svg = buildSvg(
|
|
13
|
+
{ set: 'lucide', name: 'house', width: 24, height: 24, body: '<path stroke="currentColor" d="…"/>' },
|
|
14
|
+
{ color: '#4f46e5', strokeWidth: 1.5, size: 32 },
|
|
15
|
+
)
|
|
16
|
+
const jsx = svgToJsx(svg, 'House')
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Browser-only helpers (PNG export, safe SVG upload cleaning) live in `@rdicons/core/browser`.
|
|
20
|
+
|
|
21
|
+
Looking for ready-made icons? See [`@rdicons/react`](https://www.npmjs.com/package/@rdicons/react) and the `rdicons` CLI.
|
|
22
|
+
|
|
23
|
+
MIT licensed. Docs: https://rdicons.vimalbhatt.com/docs
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/browser.ts
|
|
31
|
+
var browser_exports = {};
|
|
32
|
+
__export(browser_exports, {
|
|
33
|
+
IMAGE_TYPES: () => IMAGE_TYPES,
|
|
34
|
+
MAX_IMAGE_BYTES: () => MAX_IMAGE_BYTES,
|
|
35
|
+
MAX_SVG_BYTES: () => MAX_SVG_BYTES,
|
|
36
|
+
copyText: () => copyText,
|
|
37
|
+
downloadFile: () => downloadFile,
|
|
38
|
+
prepareSvgUpload: () => prepareSvgUpload,
|
|
39
|
+
readImageSize: () => readImageSize,
|
|
40
|
+
resizeRasterToPng: () => resizeRasterToPng,
|
|
41
|
+
svgToPngBlob: () => svgToPngBlob
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(browser_exports);
|
|
44
|
+
var import_dompurify = __toESM(require("dompurify"), 1);
|
|
45
|
+
var MAX_SVG_BYTES = 512 * 1024;
|
|
46
|
+
var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
47
|
+
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
48
|
+
var ROOT_PRESENTATION = ["fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "fill-rule", "clip-rule", "opacity", "fill-opacity", "stroke-opacity"];
|
|
49
|
+
function prepareSvgUpload(text, opts = {}) {
|
|
50
|
+
if (new Blob([text]).size > MAX_SVG_BYTES) throw new Error("SVG is larger than 512 KB");
|
|
51
|
+
if (!/<svg[\s>]/i.test(text)) throw new Error("This file is not an SVG");
|
|
52
|
+
const clean = import_dompurify.default.sanitize(text, {
|
|
53
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
54
|
+
ADD_TAGS: ["style"],
|
|
55
|
+
FORBID_TAGS: ["script", "foreignObject", "iframe", "object", "embed", "image", "a", "animate", "set", "animateTransform", "animateMotion"],
|
|
56
|
+
FORBID_ATTR: ["onload", "onclick", "onerror", "onmouseover", "onfocus", "onbegin", "onend"]
|
|
57
|
+
});
|
|
58
|
+
const doc = new DOMParser().parseFromString(clean, "image/svg+xml");
|
|
59
|
+
const svg = doc.documentElement;
|
|
60
|
+
if (!svg || svg.nodeName.toLowerCase() !== "svg" || doc.querySelector("parsererror")) throw new Error("Could not read this SVG");
|
|
61
|
+
let minX = 0;
|
|
62
|
+
let minY = 0;
|
|
63
|
+
let width = 24;
|
|
64
|
+
let height = 24;
|
|
65
|
+
const vb = svg.getAttribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
|
|
66
|
+
if (vb && vb.length === 4 && vb.every((n) => Number.isFinite(n)) && vb[2] > 0 && vb[3] > 0) {
|
|
67
|
+
;
|
|
68
|
+
[minX, minY, width, height] = vb;
|
|
69
|
+
} else {
|
|
70
|
+
const w = parseFloat(svg.getAttribute("width") ?? "");
|
|
71
|
+
const h = parseFloat(svg.getAttribute("height") ?? "");
|
|
72
|
+
if (w > 0 && h > 0) {
|
|
73
|
+
width = w;
|
|
74
|
+
height = h;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
doc.querySelectorAll("*").forEach((el) => {
|
|
78
|
+
for (const attr of ["href", "xlink:href"]) {
|
|
79
|
+
const v = el.getAttribute(attr);
|
|
80
|
+
if (v != null && !v.startsWith("#")) el.removeAttribute(attr);
|
|
81
|
+
}
|
|
82
|
+
if (el.nodeName.toLowerCase() === "style" && el.textContent) {
|
|
83
|
+
el.textContent = el.textContent.replace(/@import[^;]*;?/gi, "").replace(/url\(\s*['"]?\s*(?!#)[^)]*\)/gi, "none");
|
|
84
|
+
}
|
|
85
|
+
const st = el.getAttribute("style");
|
|
86
|
+
if (st) el.setAttribute("style", st.replace(/url\(\s*['"]?\s*(?!#)[^)]*\)/gi, "none"));
|
|
87
|
+
});
|
|
88
|
+
const attrs = [];
|
|
89
|
+
for (const name of ROOT_PRESENTATION) {
|
|
90
|
+
const v = svg.getAttribute(name);
|
|
91
|
+
if (v != null) attrs.push(`${name}="${v.replace(/"/g, """)}"`);
|
|
92
|
+
}
|
|
93
|
+
if (opts.monochrome && !svg.hasAttribute("fill") && !svg.hasAttribute("stroke")) attrs.push('fill="currentColor"');
|
|
94
|
+
if (minX || minY) attrs.push(`transform="translate(${-minX} ${-minY})"`);
|
|
95
|
+
const ser = new XMLSerializer();
|
|
96
|
+
let body = Array.from(svg.childNodes).map((n) => ser.serializeToString(n)).join("").replace(/ xmlns(:\w+)?="[^"]*"/g, "").trim();
|
|
97
|
+
if (!body) throw new Error("This SVG is empty");
|
|
98
|
+
let wrapped = attrs.length ? `<g ${attrs.join(" ")}>${body}</g>` : body;
|
|
99
|
+
if (opts.monochrome) {
|
|
100
|
+
wrapped = wrapped.replace(/\b(fill|stroke)="(?!none|currentColor|url\()[^"]*"/gi, '$1="currentColor"');
|
|
101
|
+
wrapped = wrapped.replace(/\b(fill|stroke)\s*:\s*(?!none|currentColor|url\()[^;"}]+/gi, "$1:currentColor");
|
|
102
|
+
}
|
|
103
|
+
return { body: wrapped, width: Math.round(width * 100) / 100, height: Math.round(height * 100) / 100 };
|
|
104
|
+
}
|
|
105
|
+
async function readImageSize(file) {
|
|
106
|
+
const url = URL.createObjectURL(file);
|
|
107
|
+
try {
|
|
108
|
+
const img = new Image();
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
img.onload = () => resolve();
|
|
111
|
+
img.onerror = () => reject(new Error("Could not read this image"));
|
|
112
|
+
img.src = url;
|
|
113
|
+
});
|
|
114
|
+
return { width: img.naturalWidth, height: img.naturalHeight };
|
|
115
|
+
} finally {
|
|
116
|
+
URL.revokeObjectURL(url);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function resizeRasterToPng(src, size, background) {
|
|
120
|
+
const url = typeof src === "string" ? src : URL.createObjectURL(src);
|
|
121
|
+
try {
|
|
122
|
+
const img = new Image();
|
|
123
|
+
img.crossOrigin = "anonymous";
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
img.onload = () => resolve();
|
|
126
|
+
img.onerror = () => reject(new Error("Could not load image"));
|
|
127
|
+
img.src = url;
|
|
128
|
+
});
|
|
129
|
+
const ratio = img.naturalWidth / img.naturalHeight || 1;
|
|
130
|
+
const w = ratio >= 1 ? size : Math.round(size * ratio);
|
|
131
|
+
const h = ratio >= 1 ? Math.round(size / ratio) : size;
|
|
132
|
+
const canvas = document.createElement("canvas");
|
|
133
|
+
canvas.width = w;
|
|
134
|
+
canvas.height = h;
|
|
135
|
+
const ctx = canvas.getContext("2d");
|
|
136
|
+
if (background) {
|
|
137
|
+
ctx.fillStyle = background;
|
|
138
|
+
ctx.fillRect(0, 0, w, h);
|
|
139
|
+
}
|
|
140
|
+
ctx.imageSmoothingQuality = "high";
|
|
141
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
142
|
+
return await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("PNG export failed")), "image/png"));
|
|
143
|
+
} finally {
|
|
144
|
+
if (typeof src !== "string") URL.revokeObjectURL(url);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function svgToPngBlob(svg, size, background) {
|
|
148
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
149
|
+
try {
|
|
150
|
+
const img = new Image();
|
|
151
|
+
img.decoding = "async";
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
img.onload = () => resolve();
|
|
154
|
+
img.onerror = () => reject(new Error("Could not render SVG"));
|
|
155
|
+
img.src = url;
|
|
156
|
+
});
|
|
157
|
+
const ratio = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 1;
|
|
158
|
+
const w = ratio >= 1 ? size : Math.round(size * ratio);
|
|
159
|
+
const h = ratio >= 1 ? Math.round(size / ratio) : size;
|
|
160
|
+
const canvas = document.createElement("canvas");
|
|
161
|
+
canvas.width = w;
|
|
162
|
+
canvas.height = h;
|
|
163
|
+
const ctx = canvas.getContext("2d");
|
|
164
|
+
if (!ctx) throw new Error("Canvas not supported");
|
|
165
|
+
if (background) {
|
|
166
|
+
ctx.fillStyle = background;
|
|
167
|
+
ctx.fillRect(0, 0, w, h);
|
|
168
|
+
}
|
|
169
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
170
|
+
return await new Promise(
|
|
171
|
+
(resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("PNG export failed")), "image/png")
|
|
172
|
+
);
|
|
173
|
+
} finally {
|
|
174
|
+
URL.revokeObjectURL(url);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function downloadFile(data, filename, mime = "application/octet-stream") {
|
|
178
|
+
const blob = typeof data === "string" ? new Blob([data], { type: mime }) : data;
|
|
179
|
+
const a = document.createElement("a");
|
|
180
|
+
a.href = URL.createObjectURL(blob);
|
|
181
|
+
a.download = filename;
|
|
182
|
+
document.body.appendChild(a);
|
|
183
|
+
a.click();
|
|
184
|
+
a.remove();
|
|
185
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 2e3);
|
|
186
|
+
}
|
|
187
|
+
async function copyText(text) {
|
|
188
|
+
try {
|
|
189
|
+
await navigator.clipboard.writeText(text);
|
|
190
|
+
return true;
|
|
191
|
+
} catch {
|
|
192
|
+
const ta = document.createElement("textarea");
|
|
193
|
+
ta.value = text;
|
|
194
|
+
ta.style.position = "fixed";
|
|
195
|
+
ta.style.opacity = "0";
|
|
196
|
+
document.body.appendChild(ta);
|
|
197
|
+
ta.select();
|
|
198
|
+
const ok = document.execCommand("copy");
|
|
199
|
+
ta.remove();
|
|
200
|
+
return ok;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
IMAGE_TYPES,
|
|
206
|
+
MAX_IMAGE_BYTES,
|
|
207
|
+
MAX_SVG_BYTES,
|
|
208
|
+
copyText,
|
|
209
|
+
downloadFile,
|
|
210
|
+
prepareSvgUpload,
|
|
211
|
+
readImageSize,
|
|
212
|
+
resizeRasterToPng,
|
|
213
|
+
svgToPngBlob
|
|
214
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const MAX_SVG_BYTES: number;
|
|
2
|
+
declare const MAX_IMAGE_BYTES: number;
|
|
3
|
+
declare const IMAGE_TYPES: string[];
|
|
4
|
+
interface PreparedSvg {
|
|
5
|
+
/** Safe inner markup in Iconify format (works with buildSvg and the grid) */
|
|
6
|
+
body: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Clean an uploaded SVG and turn it into {body,width,height}.
|
|
12
|
+
* - removes scripts, event handlers, foreign objects, remote images and external links
|
|
13
|
+
* - drops @import / remote url() from <style>
|
|
14
|
+
* - optionally converts every colour to currentColor so the icon can be recoloured
|
|
15
|
+
* Throws an Error with a user-readable message when the file is not a usable SVG.
|
|
16
|
+
*/
|
|
17
|
+
declare function prepareSvgUpload(text: string, opts?: {
|
|
18
|
+
monochrome?: boolean;
|
|
19
|
+
}): PreparedSvg;
|
|
20
|
+
/** Read the pixel size of a raster image file. */
|
|
21
|
+
declare function readImageSize(file: Blob): Promise<{
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}>;
|
|
25
|
+
/** Resize a raster image URL/blob to a PNG at the given longest side. */
|
|
26
|
+
declare function resizeRasterToPng(src: Blob | string, size: number, background?: string | null): Promise<Blob>;
|
|
27
|
+
/** Rasterise an SVG string to a PNG blob at the given pixel size. */
|
|
28
|
+
declare function svgToPngBlob(svg: string, size: number, background?: string | null): Promise<Blob>;
|
|
29
|
+
/** Trigger a file download for a Blob or string. */
|
|
30
|
+
declare function downloadFile(data: Blob | string, filename: string, mime?: string): void;
|
|
31
|
+
/** Copy text to the clipboard, with a fallback for older/insecure contexts. */
|
|
32
|
+
declare function copyText(text: string): Promise<boolean>;
|
|
33
|
+
|
|
34
|
+
export { IMAGE_TYPES, MAX_IMAGE_BYTES, MAX_SVG_BYTES, type PreparedSvg, copyText, downloadFile, prepareSvgUpload, readImageSize, resizeRasterToPng, svgToPngBlob };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const MAX_SVG_BYTES: number;
|
|
2
|
+
declare const MAX_IMAGE_BYTES: number;
|
|
3
|
+
declare const IMAGE_TYPES: string[];
|
|
4
|
+
interface PreparedSvg {
|
|
5
|
+
/** Safe inner markup in Iconify format (works with buildSvg and the grid) */
|
|
6
|
+
body: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Clean an uploaded SVG and turn it into {body,width,height}.
|
|
12
|
+
* - removes scripts, event handlers, foreign objects, remote images and external links
|
|
13
|
+
* - drops @import / remote url() from <style>
|
|
14
|
+
* - optionally converts every colour to currentColor so the icon can be recoloured
|
|
15
|
+
* Throws an Error with a user-readable message when the file is not a usable SVG.
|
|
16
|
+
*/
|
|
17
|
+
declare function prepareSvgUpload(text: string, opts?: {
|
|
18
|
+
monochrome?: boolean;
|
|
19
|
+
}): PreparedSvg;
|
|
20
|
+
/** Read the pixel size of a raster image file. */
|
|
21
|
+
declare function readImageSize(file: Blob): Promise<{
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}>;
|
|
25
|
+
/** Resize a raster image URL/blob to a PNG at the given longest side. */
|
|
26
|
+
declare function resizeRasterToPng(src: Blob | string, size: number, background?: string | null): Promise<Blob>;
|
|
27
|
+
/** Rasterise an SVG string to a PNG blob at the given pixel size. */
|
|
28
|
+
declare function svgToPngBlob(svg: string, size: number, background?: string | null): Promise<Blob>;
|
|
29
|
+
/** Trigger a file download for a Blob or string. */
|
|
30
|
+
declare function downloadFile(data: Blob | string, filename: string, mime?: string): void;
|
|
31
|
+
/** Copy text to the clipboard, with a fallback for older/insecure contexts. */
|
|
32
|
+
declare function copyText(text: string): Promise<boolean>;
|
|
33
|
+
|
|
34
|
+
export { IMAGE_TYPES, MAX_IMAGE_BYTES, MAX_SVG_BYTES, type PreparedSvg, copyText, downloadFile, prepareSvgUpload, readImageSize, resizeRasterToPng, svgToPngBlob };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/browser.ts
|
|
2
|
+
import DOMPurify from "dompurify";
|
|
3
|
+
var MAX_SVG_BYTES = 512 * 1024;
|
|
4
|
+
var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
var IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
6
|
+
var ROOT_PRESENTATION = ["fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "fill-rule", "clip-rule", "opacity", "fill-opacity", "stroke-opacity"];
|
|
7
|
+
function prepareSvgUpload(text, opts = {}) {
|
|
8
|
+
if (new Blob([text]).size > MAX_SVG_BYTES) throw new Error("SVG is larger than 512 KB");
|
|
9
|
+
if (!/<svg[\s>]/i.test(text)) throw new Error("This file is not an SVG");
|
|
10
|
+
const clean = DOMPurify.sanitize(text, {
|
|
11
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
12
|
+
ADD_TAGS: ["style"],
|
|
13
|
+
FORBID_TAGS: ["script", "foreignObject", "iframe", "object", "embed", "image", "a", "animate", "set", "animateTransform", "animateMotion"],
|
|
14
|
+
FORBID_ATTR: ["onload", "onclick", "onerror", "onmouseover", "onfocus", "onbegin", "onend"]
|
|
15
|
+
});
|
|
16
|
+
const doc = new DOMParser().parseFromString(clean, "image/svg+xml");
|
|
17
|
+
const svg = doc.documentElement;
|
|
18
|
+
if (!svg || svg.nodeName.toLowerCase() !== "svg" || doc.querySelector("parsererror")) throw new Error("Could not read this SVG");
|
|
19
|
+
let minX = 0;
|
|
20
|
+
let minY = 0;
|
|
21
|
+
let width = 24;
|
|
22
|
+
let height = 24;
|
|
23
|
+
const vb = svg.getAttribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
|
|
24
|
+
if (vb && vb.length === 4 && vb.every((n) => Number.isFinite(n)) && vb[2] > 0 && vb[3] > 0) {
|
|
25
|
+
;
|
|
26
|
+
[minX, minY, width, height] = vb;
|
|
27
|
+
} else {
|
|
28
|
+
const w = parseFloat(svg.getAttribute("width") ?? "");
|
|
29
|
+
const h = parseFloat(svg.getAttribute("height") ?? "");
|
|
30
|
+
if (w > 0 && h > 0) {
|
|
31
|
+
width = w;
|
|
32
|
+
height = h;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
doc.querySelectorAll("*").forEach((el) => {
|
|
36
|
+
for (const attr of ["href", "xlink:href"]) {
|
|
37
|
+
const v = el.getAttribute(attr);
|
|
38
|
+
if (v != null && !v.startsWith("#")) el.removeAttribute(attr);
|
|
39
|
+
}
|
|
40
|
+
if (el.nodeName.toLowerCase() === "style" && el.textContent) {
|
|
41
|
+
el.textContent = el.textContent.replace(/@import[^;]*;?/gi, "").replace(/url\(\s*['"]?\s*(?!#)[^)]*\)/gi, "none");
|
|
42
|
+
}
|
|
43
|
+
const st = el.getAttribute("style");
|
|
44
|
+
if (st) el.setAttribute("style", st.replace(/url\(\s*['"]?\s*(?!#)[^)]*\)/gi, "none"));
|
|
45
|
+
});
|
|
46
|
+
const attrs = [];
|
|
47
|
+
for (const name of ROOT_PRESENTATION) {
|
|
48
|
+
const v = svg.getAttribute(name);
|
|
49
|
+
if (v != null) attrs.push(`${name}="${v.replace(/"/g, """)}"`);
|
|
50
|
+
}
|
|
51
|
+
if (opts.monochrome && !svg.hasAttribute("fill") && !svg.hasAttribute("stroke")) attrs.push('fill="currentColor"');
|
|
52
|
+
if (minX || minY) attrs.push(`transform="translate(${-minX} ${-minY})"`);
|
|
53
|
+
const ser = new XMLSerializer();
|
|
54
|
+
let body = Array.from(svg.childNodes).map((n) => ser.serializeToString(n)).join("").replace(/ xmlns(:\w+)?="[^"]*"/g, "").trim();
|
|
55
|
+
if (!body) throw new Error("This SVG is empty");
|
|
56
|
+
let wrapped = attrs.length ? `<g ${attrs.join(" ")}>${body}</g>` : body;
|
|
57
|
+
if (opts.monochrome) {
|
|
58
|
+
wrapped = wrapped.replace(/\b(fill|stroke)="(?!none|currentColor|url\()[^"]*"/gi, '$1="currentColor"');
|
|
59
|
+
wrapped = wrapped.replace(/\b(fill|stroke)\s*:\s*(?!none|currentColor|url\()[^;"}]+/gi, "$1:currentColor");
|
|
60
|
+
}
|
|
61
|
+
return { body: wrapped, width: Math.round(width * 100) / 100, height: Math.round(height * 100) / 100 };
|
|
62
|
+
}
|
|
63
|
+
async function readImageSize(file) {
|
|
64
|
+
const url = URL.createObjectURL(file);
|
|
65
|
+
try {
|
|
66
|
+
const img = new Image();
|
|
67
|
+
await new Promise((resolve, reject) => {
|
|
68
|
+
img.onload = () => resolve();
|
|
69
|
+
img.onerror = () => reject(new Error("Could not read this image"));
|
|
70
|
+
img.src = url;
|
|
71
|
+
});
|
|
72
|
+
return { width: img.naturalWidth, height: img.naturalHeight };
|
|
73
|
+
} finally {
|
|
74
|
+
URL.revokeObjectURL(url);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function resizeRasterToPng(src, size, background) {
|
|
78
|
+
const url = typeof src === "string" ? src : URL.createObjectURL(src);
|
|
79
|
+
try {
|
|
80
|
+
const img = new Image();
|
|
81
|
+
img.crossOrigin = "anonymous";
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
img.onload = () => resolve();
|
|
84
|
+
img.onerror = () => reject(new Error("Could not load image"));
|
|
85
|
+
img.src = url;
|
|
86
|
+
});
|
|
87
|
+
const ratio = img.naturalWidth / img.naturalHeight || 1;
|
|
88
|
+
const w = ratio >= 1 ? size : Math.round(size * ratio);
|
|
89
|
+
const h = ratio >= 1 ? Math.round(size / ratio) : size;
|
|
90
|
+
const canvas = document.createElement("canvas");
|
|
91
|
+
canvas.width = w;
|
|
92
|
+
canvas.height = h;
|
|
93
|
+
const ctx = canvas.getContext("2d");
|
|
94
|
+
if (background) {
|
|
95
|
+
ctx.fillStyle = background;
|
|
96
|
+
ctx.fillRect(0, 0, w, h);
|
|
97
|
+
}
|
|
98
|
+
ctx.imageSmoothingQuality = "high";
|
|
99
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
100
|
+
return await new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("PNG export failed")), "image/png"));
|
|
101
|
+
} finally {
|
|
102
|
+
if (typeof src !== "string") URL.revokeObjectURL(url);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function svgToPngBlob(svg, size, background) {
|
|
106
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
107
|
+
try {
|
|
108
|
+
const img = new Image();
|
|
109
|
+
img.decoding = "async";
|
|
110
|
+
await new Promise((resolve, reject) => {
|
|
111
|
+
img.onload = () => resolve();
|
|
112
|
+
img.onerror = () => reject(new Error("Could not render SVG"));
|
|
113
|
+
img.src = url;
|
|
114
|
+
});
|
|
115
|
+
const ratio = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 1;
|
|
116
|
+
const w = ratio >= 1 ? size : Math.round(size * ratio);
|
|
117
|
+
const h = ratio >= 1 ? Math.round(size / ratio) : size;
|
|
118
|
+
const canvas = document.createElement("canvas");
|
|
119
|
+
canvas.width = w;
|
|
120
|
+
canvas.height = h;
|
|
121
|
+
const ctx = canvas.getContext("2d");
|
|
122
|
+
if (!ctx) throw new Error("Canvas not supported");
|
|
123
|
+
if (background) {
|
|
124
|
+
ctx.fillStyle = background;
|
|
125
|
+
ctx.fillRect(0, 0, w, h);
|
|
126
|
+
}
|
|
127
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
128
|
+
return await new Promise(
|
|
129
|
+
(resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("PNG export failed")), "image/png")
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
URL.revokeObjectURL(url);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function downloadFile(data, filename, mime = "application/octet-stream") {
|
|
136
|
+
const blob = typeof data === "string" ? new Blob([data], { type: mime }) : data;
|
|
137
|
+
const a = document.createElement("a");
|
|
138
|
+
a.href = URL.createObjectURL(blob);
|
|
139
|
+
a.download = filename;
|
|
140
|
+
document.body.appendChild(a);
|
|
141
|
+
a.click();
|
|
142
|
+
a.remove();
|
|
143
|
+
setTimeout(() => URL.revokeObjectURL(a.href), 2e3);
|
|
144
|
+
}
|
|
145
|
+
async function copyText(text) {
|
|
146
|
+
try {
|
|
147
|
+
await navigator.clipboard.writeText(text);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
const ta = document.createElement("textarea");
|
|
151
|
+
ta.value = text;
|
|
152
|
+
ta.style.position = "fixed";
|
|
153
|
+
ta.style.opacity = "0";
|
|
154
|
+
document.body.appendChild(ta);
|
|
155
|
+
ta.select();
|
|
156
|
+
const ok = document.execCommand("copy");
|
|
157
|
+
ta.remove();
|
|
158
|
+
return ok;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
IMAGE_TYPES,
|
|
163
|
+
MAX_IMAGE_BYTES,
|
|
164
|
+
MAX_SVG_BYTES,
|
|
165
|
+
copyText,
|
|
166
|
+
downloadFile,
|
|
167
|
+
prepareSvgUpload,
|
|
168
|
+
readImageSize,
|
|
169
|
+
resizeRasterToPng,
|
|
170
|
+
svgToPngBlob
|
|
171
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
BRAND: () => BRAND,
|
|
24
|
+
assignComponentNames: () => assignComponentNames,
|
|
25
|
+
buildSvg: () => buildSvg,
|
|
26
|
+
cliFlags: () => cliFlags,
|
|
27
|
+
componentName: () => componentName,
|
|
28
|
+
fileName: () => fileName,
|
|
29
|
+
hexToHsv: () => hexToHsv,
|
|
30
|
+
hsvToHex: () => hsvToHex,
|
|
31
|
+
importLines: () => importLines,
|
|
32
|
+
isLight: () => isLight,
|
|
33
|
+
libPackage: () => libPackage,
|
|
34
|
+
nativeStrokeWidth: () => nativeStrokeWidth,
|
|
35
|
+
normalizeHex: () => normalizeHex,
|
|
36
|
+
propsString: () => propsString,
|
|
37
|
+
snippetsFor: () => snippetsFor,
|
|
38
|
+
supportsStrokeWidth: () => supportsStrokeWidth,
|
|
39
|
+
svgToJsx: () => svgToJsx,
|
|
40
|
+
toPascalCase: () => toPascalCase
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
|
+
|
|
44
|
+
// src/svg.ts
|
|
45
|
+
var STROKE_WIDTH_RE = /stroke-width="[^"]*"/g;
|
|
46
|
+
function supportsStrokeWidth(body) {
|
|
47
|
+
return /stroke-width="/.test(body);
|
|
48
|
+
}
|
|
49
|
+
function nativeStrokeWidth(body) {
|
|
50
|
+
const m = body.match(/stroke-width="([\d.]+)"/);
|
|
51
|
+
return m ? parseFloat(m[1]) : null;
|
|
52
|
+
}
|
|
53
|
+
function buildSvg(icon, opts = {}) {
|
|
54
|
+
const { color = "#000000", strokeWidth = null, size = 24, background = null, padding = 0, keepCurrentColor = false } = opts;
|
|
55
|
+
const w = icon.width || 24;
|
|
56
|
+
const h = icon.height || 24;
|
|
57
|
+
let body = icon.body;
|
|
58
|
+
if (!keepCurrentColor) body = body.replace(/currentColor/g, color);
|
|
59
|
+
if (strokeWidth != null && supportsStrokeWidth(body)) {
|
|
60
|
+
body = body.replace(STROKE_WIDTH_RE, `stroke-width="${strokeWidth}"`);
|
|
61
|
+
}
|
|
62
|
+
const vbX = -padding;
|
|
63
|
+
const vbY = -padding;
|
|
64
|
+
const vbW = w + padding * 2;
|
|
65
|
+
const vbH = h + padding * 2;
|
|
66
|
+
const px = Math.round(size * vbW / Math.max(vbW, vbH));
|
|
67
|
+
const py = Math.round(size * vbH / Math.max(vbW, vbH));
|
|
68
|
+
const bg = background ? `<rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="${background}"/>` : "";
|
|
69
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${py}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}">${bg}${body}</svg>`;
|
|
70
|
+
}
|
|
71
|
+
var JSX_ATTRS = {
|
|
72
|
+
class: "className",
|
|
73
|
+
"stroke-width": "strokeWidth",
|
|
74
|
+
"stroke-linecap": "strokeLinecap",
|
|
75
|
+
"stroke-linejoin": "strokeLinejoin",
|
|
76
|
+
"stroke-miterlimit": "strokeMiterlimit",
|
|
77
|
+
"stroke-opacity": "strokeOpacity",
|
|
78
|
+
"stroke-dasharray": "strokeDasharray",
|
|
79
|
+
"stroke-dashoffset": "strokeDashoffset",
|
|
80
|
+
"fill-rule": "fillRule",
|
|
81
|
+
"fill-opacity": "fillOpacity",
|
|
82
|
+
"clip-rule": "clipRule",
|
|
83
|
+
"clip-path": "clipPath",
|
|
84
|
+
"stop-color": "stopColor",
|
|
85
|
+
"stop-opacity": "stopOpacity",
|
|
86
|
+
"xmlns:xlink": "xmlnsXlink",
|
|
87
|
+
"xlink:href": "xlinkHref"
|
|
88
|
+
};
|
|
89
|
+
function svgToJsx(svg, componentName2 = "Icon") {
|
|
90
|
+
const jsx = svg.replace(/\s([a-zA-Z:-]+)=/g, (_m, attr) => ` ${JSX_ATTRS[attr] ?? attr}=`);
|
|
91
|
+
return `export function ${componentName2}(props: React.SVGProps<SVGSVGElement>) {
|
|
92
|
+
return (
|
|
93
|
+
${jsx.replace("<svg ", "<svg {...props} ")}
|
|
94
|
+
)
|
|
95
|
+
}`;
|
|
96
|
+
}
|
|
97
|
+
function toPascalCase(name) {
|
|
98
|
+
const pascal = name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
|
|
99
|
+
if (!pascal) return "Icon";
|
|
100
|
+
return /^[0-9]/.test(pascal) ? "Icon" + pascal : pascal;
|
|
101
|
+
}
|
|
102
|
+
function assignComponentNames(names) {
|
|
103
|
+
const used = /* @__PURE__ */ new Set();
|
|
104
|
+
return names.map((name) => {
|
|
105
|
+
const base = toPascalCase(name);
|
|
106
|
+
let comp = base;
|
|
107
|
+
for (let n = 2; used.has(comp); n++) comp = `${base}_${n}`;
|
|
108
|
+
used.add(comp);
|
|
109
|
+
return comp;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function fileName(icon, ext) {
|
|
113
|
+
return `${icon.set}-${icon.name}.${ext}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/color.ts
|
|
117
|
+
var clamp = (n, a = 0, b = 1) => Math.min(b, Math.max(a, n));
|
|
118
|
+
function normalizeHex(input) {
|
|
119
|
+
const m = input.trim().match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
120
|
+
if (!m) return null;
|
|
121
|
+
let h = m[1].toLowerCase();
|
|
122
|
+
if (h.length === 3) h = [...h].map((c) => c + c).join("");
|
|
123
|
+
return "#" + h;
|
|
124
|
+
}
|
|
125
|
+
function hexToHsv(hex) {
|
|
126
|
+
const n = normalizeHex(hex) ?? "#000000";
|
|
127
|
+
const r = parseInt(n.slice(1, 3), 16) / 255;
|
|
128
|
+
const g = parseInt(n.slice(3, 5), 16) / 255;
|
|
129
|
+
const b = parseInt(n.slice(5, 7), 16) / 255;
|
|
130
|
+
const max = Math.max(r, g, b);
|
|
131
|
+
const min = Math.min(r, g, b);
|
|
132
|
+
const d = max - min;
|
|
133
|
+
let h = 0;
|
|
134
|
+
if (d) {
|
|
135
|
+
if (max === r) h = (g - b) / d % 6;
|
|
136
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
137
|
+
else h = (r - g) / d + 4;
|
|
138
|
+
h *= 60;
|
|
139
|
+
if (h < 0) h += 360;
|
|
140
|
+
}
|
|
141
|
+
return { h, s: max === 0 ? 0 : d / max, v: max };
|
|
142
|
+
}
|
|
143
|
+
function hsvToHex({ h, s, v }) {
|
|
144
|
+
const hh = (h % 360 + 360) % 360;
|
|
145
|
+
const ss = clamp(s);
|
|
146
|
+
const vv = clamp(v);
|
|
147
|
+
const c = vv * ss;
|
|
148
|
+
const x = c * (1 - Math.abs(hh / 60 % 2 - 1));
|
|
149
|
+
const m = vv - c;
|
|
150
|
+
const [r, g, b] = hh < 60 ? [c, x, 0] : hh < 120 ? [x, c, 0] : hh < 180 ? [0, c, x] : hh < 240 ? [0, x, c] : hh < 300 ? [x, 0, c] : [c, 0, x];
|
|
151
|
+
const to = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0");
|
|
152
|
+
return `#${to(r)}${to(g)}${to(b)}`;
|
|
153
|
+
}
|
|
154
|
+
function isLight(hex) {
|
|
155
|
+
const n = normalizeHex(hex) ?? "#000000";
|
|
156
|
+
const r = parseInt(n.slice(1, 3), 16);
|
|
157
|
+
const g = parseInt(n.slice(3, 5), 16);
|
|
158
|
+
const b = parseInt(n.slice(5, 7), 16);
|
|
159
|
+
return (r * 299 + g * 587 + b * 114) / 1e3 > 160;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/brand.ts
|
|
163
|
+
var BRAND = {
|
|
164
|
+
name: "rdicons",
|
|
165
|
+
tagline: "Free icons you can customise and export",
|
|
166
|
+
description: "Browse thousands of open-source icons, change colour, stroke and size, then copy or download them as SVG or PNG.",
|
|
167
|
+
/** Public address. Override with NEXT_PUBLIC_SITE_URL (for example http://localhost:3000 while developing). */
|
|
168
|
+
siteUrl: "https://rdicons.vimalbhatt.com",
|
|
169
|
+
repoUrl: "https://github.com/DevHydeOut/rdicons-lib",
|
|
170
|
+
keywords: ["free icons", "svg icons", "icon library", "icon customizer", "icon generator", "svg to png", "react icons", "figma icons", "open source icons", "lucide", "tabler icons", "heroicons", "material symbols"]
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/snippets.ts
|
|
174
|
+
var libPackage = (set) => `@rdicons/react-${set}`;
|
|
175
|
+
function propsString(o = {}) {
|
|
176
|
+
const p = [];
|
|
177
|
+
if (o.size && o.size !== 24) p.push(`size={${o.size}}`);
|
|
178
|
+
if (o.color) p.push(`color="${o.color}"`);
|
|
179
|
+
if (o.strokeWidth != null) p.push(`strokeWidth={${o.strokeWidth}}`);
|
|
180
|
+
if (o.animate) p.push(o.animate === "hover" ? "animate" : `animate="${o.animate}"`);
|
|
181
|
+
return p.length ? " " + p.join(" ") : "";
|
|
182
|
+
}
|
|
183
|
+
function componentName(icon) {
|
|
184
|
+
if (icon.c) return icon.c;
|
|
185
|
+
const p = icon.n.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((x) => x[0].toUpperCase() + x.slice(1)).join("");
|
|
186
|
+
return /^[0-9]/.test(p) ? "Icon" + p : p || "Icon";
|
|
187
|
+
}
|
|
188
|
+
function cliFlags(o = {}) {
|
|
189
|
+
const f = [];
|
|
190
|
+
if (o.color) f.push(`--color "${o.color}"`);
|
|
191
|
+
if (o.size && o.size !== 24) f.push(`--size ${o.size}`);
|
|
192
|
+
if (o.strokeWidth != null) f.push(`--stroke ${o.strokeWidth}`);
|
|
193
|
+
return f.length ? " " + f.join(" ") : "";
|
|
194
|
+
}
|
|
195
|
+
function snippetsFor(icon, o = {}) {
|
|
196
|
+
const C = componentName(icon);
|
|
197
|
+
const props = propsString(o);
|
|
198
|
+
const pkg = libPackage(icon.s);
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
id: "lib",
|
|
202
|
+
title: `React: only ${icon.s}`,
|
|
203
|
+
note: "Smallest install: just this one library.",
|
|
204
|
+
install: `npm install ${pkg}`,
|
|
205
|
+
lang: "tsx",
|
|
206
|
+
code: `import { ${C} } from '${pkg}'
|
|
207
|
+
|
|
208
|
+
export function Example() {
|
|
209
|
+
return <${C}${props} />
|
|
210
|
+
}`
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "all",
|
|
214
|
+
title: "React: all libraries",
|
|
215
|
+
note: "One package with every library. Only the icons you import are bundled.",
|
|
216
|
+
install: "npm install @rdicons/react",
|
|
217
|
+
lang: "tsx",
|
|
218
|
+
code: `import { ${C} } from '@rdicons/react/${icon.s}'
|
|
219
|
+
|
|
220
|
+
export function Example() {
|
|
221
|
+
return <${C}${props} />
|
|
222
|
+
}`
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "cli",
|
|
226
|
+
title: "Just this one icon",
|
|
227
|
+
note: "Copies the icon into your project as a file. Nothing to install, works with any framework.",
|
|
228
|
+
install: `npx rdicons add ${icon.s}:${icon.n} --format tsx --out src/icons${cliFlags(o)}`,
|
|
229
|
+
lang: "tsx",
|
|
230
|
+
code: `import { ${C} } from './icons/${C}'
|
|
231
|
+
|
|
232
|
+
export function Example() {
|
|
233
|
+
return <${C} />
|
|
234
|
+
}`
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
function importLines(icons) {
|
|
239
|
+
const bySet = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const i of icons) {
|
|
241
|
+
const list = bySet.get(i.s) ?? [];
|
|
242
|
+
const c = componentName(i);
|
|
243
|
+
if (!list.includes(c)) list.push(c);
|
|
244
|
+
bySet.set(i.s, list);
|
|
245
|
+
}
|
|
246
|
+
return [...bySet.entries()].map(([set, names]) => ({
|
|
247
|
+
set,
|
|
248
|
+
pkg: libPackage(set),
|
|
249
|
+
names,
|
|
250
|
+
statement: `import { ${names.join(", ")} } from '${libPackage(set)}'`
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
254
|
+
0 && (module.exports = {
|
|
255
|
+
BRAND,
|
|
256
|
+
assignComponentNames,
|
|
257
|
+
buildSvg,
|
|
258
|
+
cliFlags,
|
|
259
|
+
componentName,
|
|
260
|
+
fileName,
|
|
261
|
+
hexToHsv,
|
|
262
|
+
hsvToHex,
|
|
263
|
+
importLines,
|
|
264
|
+
isLight,
|
|
265
|
+
libPackage,
|
|
266
|
+
nativeStrokeWidth,
|
|
267
|
+
normalizeHex,
|
|
268
|
+
propsString,
|
|
269
|
+
snippetsFor,
|
|
270
|
+
supportsStrokeWidth,
|
|
271
|
+
svgToJsx,
|
|
272
|
+
toPascalCase
|
|
273
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Minimal icon shape shared by every product (web, Figma plugin, desktop). */
|
|
2
|
+
interface IconData {
|
|
3
|
+
/** Icon set prefix, e.g. "lucide" */
|
|
4
|
+
set: string;
|
|
5
|
+
/** Icon name inside the set, e.g. "arrow-right" */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Inner SVG markup (no <svg> wrapper) in Iconify format */
|
|
8
|
+
body: string;
|
|
9
|
+
/** viewBox width (default 24) */
|
|
10
|
+
width: number;
|
|
11
|
+
/** viewBox height (default 24) */
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
interface CustomizeOptions {
|
|
15
|
+
/** Any CSS colour. Replaces `currentColor`. Default #000000 */
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Replaces every stroke-width. null/undefined keeps the icon's own value */
|
|
18
|
+
strokeWidth?: number | null;
|
|
19
|
+
/** Output width/height in px. Default 24 */
|
|
20
|
+
size?: number;
|
|
21
|
+
/** Optional solid background rectangle behind the icon */
|
|
22
|
+
background?: string | null;
|
|
23
|
+
/** Extra space around the icon in viewBox units */
|
|
24
|
+
padding?: number;
|
|
25
|
+
/** Keep `currentColor` instead of replacing it (handy for code) */
|
|
26
|
+
keepCurrentColor?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when the icon has an explicit stroke width we can override. */
|
|
30
|
+
declare function supportsStrokeWidth(body: string): boolean;
|
|
31
|
+
/** Read the first stroke-width in the icon (its native value), if any. */
|
|
32
|
+
declare function nativeStrokeWidth(body: string): number | null;
|
|
33
|
+
/** Build a standalone, paste-anywhere SVG string with the user's customisations. */
|
|
34
|
+
declare function buildSvg(icon: Pick<IconData, 'body' | 'width' | 'height'>, opts?: CustomizeOptions): string;
|
|
35
|
+
/** Convert an SVG string into a React component snippet. */
|
|
36
|
+
declare function svgToJsx(svg: string, componentName?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* "arrow-right" -> "ArrowRight". The result is always a valid JavaScript identifier:
|
|
39
|
+
* a name that starts with a digit gets an "Icon" prefix ("24-hours" -> "Icon24Hours"), an empty name becomes "Icon".
|
|
40
|
+
*/
|
|
41
|
+
declare function toPascalCase(name: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Component names for a whole icon library, in the same order as `names`.
|
|
44
|
+
* Two icons can map to the same PascalCase name ("arrow-down-01" and "arrow-down01"); the later one gets "_2", "_3"...
|
|
45
|
+
* The npm packages and the website both use this function, so the code the website shows always matches the package.
|
|
46
|
+
*/
|
|
47
|
+
declare function assignComponentNames(names: string[]): string[];
|
|
48
|
+
/** Safe file name for downloads. */
|
|
49
|
+
declare function fileName(icon: Pick<IconData, 'set' | 'name'>, ext: string): string;
|
|
50
|
+
|
|
51
|
+
/** Small, dependency-free colour maths shared by the website and the Figma plugin pickers. */
|
|
52
|
+
interface HSV {
|
|
53
|
+
/** hue 0-360 */
|
|
54
|
+
h: number;
|
|
55
|
+
/** saturation 0-1 */
|
|
56
|
+
s: number;
|
|
57
|
+
/** value (brightness) 0-1 */
|
|
58
|
+
v: number;
|
|
59
|
+
}
|
|
60
|
+
/** "#abc", "abc", "#AABBCC" -> "#aabbcc". Returns null when it is not a valid hex colour. */
|
|
61
|
+
declare function normalizeHex(input: string): string | null;
|
|
62
|
+
declare function hexToHsv(hex: string): HSV;
|
|
63
|
+
declare function hsvToHex({ h, s, v }: HSV): string;
|
|
64
|
+
/** True for light colours (useful to pick a readable outline). */
|
|
65
|
+
declare function isLight(hex: string): boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Single place to rename the product. Change `name` (and the domain env var)
|
|
69
|
+
* and the whole site, metadata and logo text follow.
|
|
70
|
+
*/
|
|
71
|
+
declare const BRAND: {
|
|
72
|
+
readonly name: "rdicons";
|
|
73
|
+
readonly tagline: "Free icons you can customise and export";
|
|
74
|
+
readonly description: "Browse thousands of open-source icons, change colour, stroke and size, then copy or download them as SVG or PNG.";
|
|
75
|
+
/** Public address. Override with NEXT_PUBLIC_SITE_URL (for example http://localhost:3000 while developing). */
|
|
76
|
+
readonly siteUrl: "https://rdicons.vimalbhatt.com";
|
|
77
|
+
readonly repoUrl: "https://github.com/DevHydeOut/rdicons-lib";
|
|
78
|
+
readonly keywords: readonly ["free icons", "svg icons", "icon library", "icon customizer", "icon generator", "svg to png", "react icons", "figma icons", "open source icons", "lucide", "tabler icons", "heroicons", "material symbols"];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Minimal icon reference: library prefix, icon name and the React component name. */
|
|
82
|
+
interface IconRef {
|
|
83
|
+
s: string;
|
|
84
|
+
n: string;
|
|
85
|
+
c?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The code a developer needs to use one icon, for every way of installing rdicons.
|
|
89
|
+
* Used by the website ("Use in code"), the AI assistant and the docs, so they never disagree.
|
|
90
|
+
*/
|
|
91
|
+
interface UseOptions {
|
|
92
|
+
/** px; the component default is 24 */
|
|
93
|
+
size?: number;
|
|
94
|
+
/** any CSS color; omit to inherit the text color */
|
|
95
|
+
color?: string | null;
|
|
96
|
+
strokeWidth?: number | null;
|
|
97
|
+
/** when the icon has an animation: the trigger to show in the React snippet (hover is written as plain `animate`) */
|
|
98
|
+
animate?: 'hover' | 'parent' | 'play' | 'loop';
|
|
99
|
+
}
|
|
100
|
+
interface Snippet {
|
|
101
|
+
id: string;
|
|
102
|
+
title: string;
|
|
103
|
+
note?: string;
|
|
104
|
+
/** shell lines, shown first */
|
|
105
|
+
install?: string;
|
|
106
|
+
code: string;
|
|
107
|
+
lang: string;
|
|
108
|
+
}
|
|
109
|
+
declare const libPackage: (set: string) => string;
|
|
110
|
+
/** JSX props for the current customization; defaults are left out so the snippet stays short. */
|
|
111
|
+
declare function propsString(o?: UseOptions): string;
|
|
112
|
+
/** Component name, e.g. "ArrowRight". Falls back to a PascalCase guess when the server did not send it. */
|
|
113
|
+
declare function componentName(icon: Pick<IconRef, 'n' | 'c'>): string;
|
|
114
|
+
declare function cliFlags(o?: UseOptions): string;
|
|
115
|
+
/** The three ways to use ONE icon from a library, plus plain SVG. */
|
|
116
|
+
declare function snippetsFor(icon: Pick<IconRef, 's' | 'n' | 'c'>, o?: UseOptions): Snippet[];
|
|
117
|
+
/** One import statement per library for a list of icons (used for AI project suggestions). */
|
|
118
|
+
declare function importLines(icons: Pick<IconRef, 's' | 'n' | 'c'>[]): {
|
|
119
|
+
set: string;
|
|
120
|
+
pkg: string;
|
|
121
|
+
names: string[];
|
|
122
|
+
statement: string;
|
|
123
|
+
}[];
|
|
124
|
+
|
|
125
|
+
export { BRAND, type CustomizeOptions, type HSV, type IconData, type IconRef, type Snippet, type UseOptions, assignComponentNames, buildSvg, cliFlags, componentName, fileName, hexToHsv, hsvToHex, importLines, isLight, libPackage, nativeStrokeWidth, normalizeHex, propsString, snippetsFor, supportsStrokeWidth, svgToJsx, toPascalCase };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Minimal icon shape shared by every product (web, Figma plugin, desktop). */
|
|
2
|
+
interface IconData {
|
|
3
|
+
/** Icon set prefix, e.g. "lucide" */
|
|
4
|
+
set: string;
|
|
5
|
+
/** Icon name inside the set, e.g. "arrow-right" */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Inner SVG markup (no <svg> wrapper) in Iconify format */
|
|
8
|
+
body: string;
|
|
9
|
+
/** viewBox width (default 24) */
|
|
10
|
+
width: number;
|
|
11
|
+
/** viewBox height (default 24) */
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
interface CustomizeOptions {
|
|
15
|
+
/** Any CSS colour. Replaces `currentColor`. Default #000000 */
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Replaces every stroke-width. null/undefined keeps the icon's own value */
|
|
18
|
+
strokeWidth?: number | null;
|
|
19
|
+
/** Output width/height in px. Default 24 */
|
|
20
|
+
size?: number;
|
|
21
|
+
/** Optional solid background rectangle behind the icon */
|
|
22
|
+
background?: string | null;
|
|
23
|
+
/** Extra space around the icon in viewBox units */
|
|
24
|
+
padding?: number;
|
|
25
|
+
/** Keep `currentColor` instead of replacing it (handy for code) */
|
|
26
|
+
keepCurrentColor?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** True when the icon has an explicit stroke width we can override. */
|
|
30
|
+
declare function supportsStrokeWidth(body: string): boolean;
|
|
31
|
+
/** Read the first stroke-width in the icon (its native value), if any. */
|
|
32
|
+
declare function nativeStrokeWidth(body: string): number | null;
|
|
33
|
+
/** Build a standalone, paste-anywhere SVG string with the user's customisations. */
|
|
34
|
+
declare function buildSvg(icon: Pick<IconData, 'body' | 'width' | 'height'>, opts?: CustomizeOptions): string;
|
|
35
|
+
/** Convert an SVG string into a React component snippet. */
|
|
36
|
+
declare function svgToJsx(svg: string, componentName?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* "arrow-right" -> "ArrowRight". The result is always a valid JavaScript identifier:
|
|
39
|
+
* a name that starts with a digit gets an "Icon" prefix ("24-hours" -> "Icon24Hours"), an empty name becomes "Icon".
|
|
40
|
+
*/
|
|
41
|
+
declare function toPascalCase(name: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Component names for a whole icon library, in the same order as `names`.
|
|
44
|
+
* Two icons can map to the same PascalCase name ("arrow-down-01" and "arrow-down01"); the later one gets "_2", "_3"...
|
|
45
|
+
* The npm packages and the website both use this function, so the code the website shows always matches the package.
|
|
46
|
+
*/
|
|
47
|
+
declare function assignComponentNames(names: string[]): string[];
|
|
48
|
+
/** Safe file name for downloads. */
|
|
49
|
+
declare function fileName(icon: Pick<IconData, 'set' | 'name'>, ext: string): string;
|
|
50
|
+
|
|
51
|
+
/** Small, dependency-free colour maths shared by the website and the Figma plugin pickers. */
|
|
52
|
+
interface HSV {
|
|
53
|
+
/** hue 0-360 */
|
|
54
|
+
h: number;
|
|
55
|
+
/** saturation 0-1 */
|
|
56
|
+
s: number;
|
|
57
|
+
/** value (brightness) 0-1 */
|
|
58
|
+
v: number;
|
|
59
|
+
}
|
|
60
|
+
/** "#abc", "abc", "#AABBCC" -> "#aabbcc". Returns null when it is not a valid hex colour. */
|
|
61
|
+
declare function normalizeHex(input: string): string | null;
|
|
62
|
+
declare function hexToHsv(hex: string): HSV;
|
|
63
|
+
declare function hsvToHex({ h, s, v }: HSV): string;
|
|
64
|
+
/** True for light colours (useful to pick a readable outline). */
|
|
65
|
+
declare function isLight(hex: string): boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Single place to rename the product. Change `name` (and the domain env var)
|
|
69
|
+
* and the whole site, metadata and logo text follow.
|
|
70
|
+
*/
|
|
71
|
+
declare const BRAND: {
|
|
72
|
+
readonly name: "rdicons";
|
|
73
|
+
readonly tagline: "Free icons you can customise and export";
|
|
74
|
+
readonly description: "Browse thousands of open-source icons, change colour, stroke and size, then copy or download them as SVG or PNG.";
|
|
75
|
+
/** Public address. Override with NEXT_PUBLIC_SITE_URL (for example http://localhost:3000 while developing). */
|
|
76
|
+
readonly siteUrl: "https://rdicons.vimalbhatt.com";
|
|
77
|
+
readonly repoUrl: "https://github.com/DevHydeOut/rdicons-lib";
|
|
78
|
+
readonly keywords: readonly ["free icons", "svg icons", "icon library", "icon customizer", "icon generator", "svg to png", "react icons", "figma icons", "open source icons", "lucide", "tabler icons", "heroicons", "material symbols"];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Minimal icon reference: library prefix, icon name and the React component name. */
|
|
82
|
+
interface IconRef {
|
|
83
|
+
s: string;
|
|
84
|
+
n: string;
|
|
85
|
+
c?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The code a developer needs to use one icon, for every way of installing rdicons.
|
|
89
|
+
* Used by the website ("Use in code"), the AI assistant and the docs, so they never disagree.
|
|
90
|
+
*/
|
|
91
|
+
interface UseOptions {
|
|
92
|
+
/** px; the component default is 24 */
|
|
93
|
+
size?: number;
|
|
94
|
+
/** any CSS color; omit to inherit the text color */
|
|
95
|
+
color?: string | null;
|
|
96
|
+
strokeWidth?: number | null;
|
|
97
|
+
/** when the icon has an animation: the trigger to show in the React snippet (hover is written as plain `animate`) */
|
|
98
|
+
animate?: 'hover' | 'parent' | 'play' | 'loop';
|
|
99
|
+
}
|
|
100
|
+
interface Snippet {
|
|
101
|
+
id: string;
|
|
102
|
+
title: string;
|
|
103
|
+
note?: string;
|
|
104
|
+
/** shell lines, shown first */
|
|
105
|
+
install?: string;
|
|
106
|
+
code: string;
|
|
107
|
+
lang: string;
|
|
108
|
+
}
|
|
109
|
+
declare const libPackage: (set: string) => string;
|
|
110
|
+
/** JSX props for the current customization; defaults are left out so the snippet stays short. */
|
|
111
|
+
declare function propsString(o?: UseOptions): string;
|
|
112
|
+
/** Component name, e.g. "ArrowRight". Falls back to a PascalCase guess when the server did not send it. */
|
|
113
|
+
declare function componentName(icon: Pick<IconRef, 'n' | 'c'>): string;
|
|
114
|
+
declare function cliFlags(o?: UseOptions): string;
|
|
115
|
+
/** The three ways to use ONE icon from a library, plus plain SVG. */
|
|
116
|
+
declare function snippetsFor(icon: Pick<IconRef, 's' | 'n' | 'c'>, o?: UseOptions): Snippet[];
|
|
117
|
+
/** One import statement per library for a list of icons (used for AI project suggestions). */
|
|
118
|
+
declare function importLines(icons: Pick<IconRef, 's' | 'n' | 'c'>[]): {
|
|
119
|
+
set: string;
|
|
120
|
+
pkg: string;
|
|
121
|
+
names: string[];
|
|
122
|
+
statement: string;
|
|
123
|
+
}[];
|
|
124
|
+
|
|
125
|
+
export { BRAND, type CustomizeOptions, type HSV, type IconData, type IconRef, type Snippet, type UseOptions, assignComponentNames, buildSvg, cliFlags, componentName, fileName, hexToHsv, hsvToHex, importLines, isLight, libPackage, nativeStrokeWidth, normalizeHex, propsString, snippetsFor, supportsStrokeWidth, svgToJsx, toPascalCase };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// src/svg.ts
|
|
2
|
+
var STROKE_WIDTH_RE = /stroke-width="[^"]*"/g;
|
|
3
|
+
function supportsStrokeWidth(body) {
|
|
4
|
+
return /stroke-width="/.test(body);
|
|
5
|
+
}
|
|
6
|
+
function nativeStrokeWidth(body) {
|
|
7
|
+
const m = body.match(/stroke-width="([\d.]+)"/);
|
|
8
|
+
return m ? parseFloat(m[1]) : null;
|
|
9
|
+
}
|
|
10
|
+
function buildSvg(icon, opts = {}) {
|
|
11
|
+
const { color = "#000000", strokeWidth = null, size = 24, background = null, padding = 0, keepCurrentColor = false } = opts;
|
|
12
|
+
const w = icon.width || 24;
|
|
13
|
+
const h = icon.height || 24;
|
|
14
|
+
let body = icon.body;
|
|
15
|
+
if (!keepCurrentColor) body = body.replace(/currentColor/g, color);
|
|
16
|
+
if (strokeWidth != null && supportsStrokeWidth(body)) {
|
|
17
|
+
body = body.replace(STROKE_WIDTH_RE, `stroke-width="${strokeWidth}"`);
|
|
18
|
+
}
|
|
19
|
+
const vbX = -padding;
|
|
20
|
+
const vbY = -padding;
|
|
21
|
+
const vbW = w + padding * 2;
|
|
22
|
+
const vbH = h + padding * 2;
|
|
23
|
+
const px = Math.round(size * vbW / Math.max(vbW, vbH));
|
|
24
|
+
const py = Math.round(size * vbH / Math.max(vbW, vbH));
|
|
25
|
+
const bg = background ? `<rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="${background}"/>` : "";
|
|
26
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${py}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}">${bg}${body}</svg>`;
|
|
27
|
+
}
|
|
28
|
+
var JSX_ATTRS = {
|
|
29
|
+
class: "className",
|
|
30
|
+
"stroke-width": "strokeWidth",
|
|
31
|
+
"stroke-linecap": "strokeLinecap",
|
|
32
|
+
"stroke-linejoin": "strokeLinejoin",
|
|
33
|
+
"stroke-miterlimit": "strokeMiterlimit",
|
|
34
|
+
"stroke-opacity": "strokeOpacity",
|
|
35
|
+
"stroke-dasharray": "strokeDasharray",
|
|
36
|
+
"stroke-dashoffset": "strokeDashoffset",
|
|
37
|
+
"fill-rule": "fillRule",
|
|
38
|
+
"fill-opacity": "fillOpacity",
|
|
39
|
+
"clip-rule": "clipRule",
|
|
40
|
+
"clip-path": "clipPath",
|
|
41
|
+
"stop-color": "stopColor",
|
|
42
|
+
"stop-opacity": "stopOpacity",
|
|
43
|
+
"xmlns:xlink": "xmlnsXlink",
|
|
44
|
+
"xlink:href": "xlinkHref"
|
|
45
|
+
};
|
|
46
|
+
function svgToJsx(svg, componentName2 = "Icon") {
|
|
47
|
+
const jsx = svg.replace(/\s([a-zA-Z:-]+)=/g, (_m, attr) => ` ${JSX_ATTRS[attr] ?? attr}=`);
|
|
48
|
+
return `export function ${componentName2}(props: React.SVGProps<SVGSVGElement>) {
|
|
49
|
+
return (
|
|
50
|
+
${jsx.replace("<svg ", "<svg {...props} ")}
|
|
51
|
+
)
|
|
52
|
+
}`;
|
|
53
|
+
}
|
|
54
|
+
function toPascalCase(name) {
|
|
55
|
+
const pascal = name.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
|
|
56
|
+
if (!pascal) return "Icon";
|
|
57
|
+
return /^[0-9]/.test(pascal) ? "Icon" + pascal : pascal;
|
|
58
|
+
}
|
|
59
|
+
function assignComponentNames(names) {
|
|
60
|
+
const used = /* @__PURE__ */ new Set();
|
|
61
|
+
return names.map((name) => {
|
|
62
|
+
const base = toPascalCase(name);
|
|
63
|
+
let comp = base;
|
|
64
|
+
for (let n = 2; used.has(comp); n++) comp = `${base}_${n}`;
|
|
65
|
+
used.add(comp);
|
|
66
|
+
return comp;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function fileName(icon, ext) {
|
|
70
|
+
return `${icon.set}-${icon.name}.${ext}`.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/color.ts
|
|
74
|
+
var clamp = (n, a = 0, b = 1) => Math.min(b, Math.max(a, n));
|
|
75
|
+
function normalizeHex(input) {
|
|
76
|
+
const m = input.trim().match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
77
|
+
if (!m) return null;
|
|
78
|
+
let h = m[1].toLowerCase();
|
|
79
|
+
if (h.length === 3) h = [...h].map((c) => c + c).join("");
|
|
80
|
+
return "#" + h;
|
|
81
|
+
}
|
|
82
|
+
function hexToHsv(hex) {
|
|
83
|
+
const n = normalizeHex(hex) ?? "#000000";
|
|
84
|
+
const r = parseInt(n.slice(1, 3), 16) / 255;
|
|
85
|
+
const g = parseInt(n.slice(3, 5), 16) / 255;
|
|
86
|
+
const b = parseInt(n.slice(5, 7), 16) / 255;
|
|
87
|
+
const max = Math.max(r, g, b);
|
|
88
|
+
const min = Math.min(r, g, b);
|
|
89
|
+
const d = max - min;
|
|
90
|
+
let h = 0;
|
|
91
|
+
if (d) {
|
|
92
|
+
if (max === r) h = (g - b) / d % 6;
|
|
93
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
94
|
+
else h = (r - g) / d + 4;
|
|
95
|
+
h *= 60;
|
|
96
|
+
if (h < 0) h += 360;
|
|
97
|
+
}
|
|
98
|
+
return { h, s: max === 0 ? 0 : d / max, v: max };
|
|
99
|
+
}
|
|
100
|
+
function hsvToHex({ h, s, v }) {
|
|
101
|
+
const hh = (h % 360 + 360) % 360;
|
|
102
|
+
const ss = clamp(s);
|
|
103
|
+
const vv = clamp(v);
|
|
104
|
+
const c = vv * ss;
|
|
105
|
+
const x = c * (1 - Math.abs(hh / 60 % 2 - 1));
|
|
106
|
+
const m = vv - c;
|
|
107
|
+
const [r, g, b] = hh < 60 ? [c, x, 0] : hh < 120 ? [x, c, 0] : hh < 180 ? [0, c, x] : hh < 240 ? [0, x, c] : hh < 300 ? [x, 0, c] : [c, 0, x];
|
|
108
|
+
const to = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0");
|
|
109
|
+
return `#${to(r)}${to(g)}${to(b)}`;
|
|
110
|
+
}
|
|
111
|
+
function isLight(hex) {
|
|
112
|
+
const n = normalizeHex(hex) ?? "#000000";
|
|
113
|
+
const r = parseInt(n.slice(1, 3), 16);
|
|
114
|
+
const g = parseInt(n.slice(3, 5), 16);
|
|
115
|
+
const b = parseInt(n.slice(5, 7), 16);
|
|
116
|
+
return (r * 299 + g * 587 + b * 114) / 1e3 > 160;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/brand.ts
|
|
120
|
+
var BRAND = {
|
|
121
|
+
name: "rdicons",
|
|
122
|
+
tagline: "Free icons you can customise and export",
|
|
123
|
+
description: "Browse thousands of open-source icons, change colour, stroke and size, then copy or download them as SVG or PNG.",
|
|
124
|
+
/** Public address. Override with NEXT_PUBLIC_SITE_URL (for example http://localhost:3000 while developing). */
|
|
125
|
+
siteUrl: "https://rdicons.vimalbhatt.com",
|
|
126
|
+
repoUrl: "https://github.com/DevHydeOut/rdicons-lib",
|
|
127
|
+
keywords: ["free icons", "svg icons", "icon library", "icon customizer", "icon generator", "svg to png", "react icons", "figma icons", "open source icons", "lucide", "tabler icons", "heroicons", "material symbols"]
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/snippets.ts
|
|
131
|
+
var libPackage = (set) => `@rdicons/react-${set}`;
|
|
132
|
+
function propsString(o = {}) {
|
|
133
|
+
const p = [];
|
|
134
|
+
if (o.size && o.size !== 24) p.push(`size={${o.size}}`);
|
|
135
|
+
if (o.color) p.push(`color="${o.color}"`);
|
|
136
|
+
if (o.strokeWidth != null) p.push(`strokeWidth={${o.strokeWidth}}`);
|
|
137
|
+
if (o.animate) p.push(o.animate === "hover" ? "animate" : `animate="${o.animate}"`);
|
|
138
|
+
return p.length ? " " + p.join(" ") : "";
|
|
139
|
+
}
|
|
140
|
+
function componentName(icon) {
|
|
141
|
+
if (icon.c) return icon.c;
|
|
142
|
+
const p = icon.n.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((x) => x[0].toUpperCase() + x.slice(1)).join("");
|
|
143
|
+
return /^[0-9]/.test(p) ? "Icon" + p : p || "Icon";
|
|
144
|
+
}
|
|
145
|
+
function cliFlags(o = {}) {
|
|
146
|
+
const f = [];
|
|
147
|
+
if (o.color) f.push(`--color "${o.color}"`);
|
|
148
|
+
if (o.size && o.size !== 24) f.push(`--size ${o.size}`);
|
|
149
|
+
if (o.strokeWidth != null) f.push(`--stroke ${o.strokeWidth}`);
|
|
150
|
+
return f.length ? " " + f.join(" ") : "";
|
|
151
|
+
}
|
|
152
|
+
function snippetsFor(icon, o = {}) {
|
|
153
|
+
const C = componentName(icon);
|
|
154
|
+
const props = propsString(o);
|
|
155
|
+
const pkg = libPackage(icon.s);
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
id: "lib",
|
|
159
|
+
title: `React: only ${icon.s}`,
|
|
160
|
+
note: "Smallest install: just this one library.",
|
|
161
|
+
install: `npm install ${pkg}`,
|
|
162
|
+
lang: "tsx",
|
|
163
|
+
code: `import { ${C} } from '${pkg}'
|
|
164
|
+
|
|
165
|
+
export function Example() {
|
|
166
|
+
return <${C}${props} />
|
|
167
|
+
}`
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "all",
|
|
171
|
+
title: "React: all libraries",
|
|
172
|
+
note: "One package with every library. Only the icons you import are bundled.",
|
|
173
|
+
install: "npm install @rdicons/react",
|
|
174
|
+
lang: "tsx",
|
|
175
|
+
code: `import { ${C} } from '@rdicons/react/${icon.s}'
|
|
176
|
+
|
|
177
|
+
export function Example() {
|
|
178
|
+
return <${C}${props} />
|
|
179
|
+
}`
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "cli",
|
|
183
|
+
title: "Just this one icon",
|
|
184
|
+
note: "Copies the icon into your project as a file. Nothing to install, works with any framework.",
|
|
185
|
+
install: `npx rdicons add ${icon.s}:${icon.n} --format tsx --out src/icons${cliFlags(o)}`,
|
|
186
|
+
lang: "tsx",
|
|
187
|
+
code: `import { ${C} } from './icons/${C}'
|
|
188
|
+
|
|
189
|
+
export function Example() {
|
|
190
|
+
return <${C} />
|
|
191
|
+
}`
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
function importLines(icons) {
|
|
196
|
+
const bySet = /* @__PURE__ */ new Map();
|
|
197
|
+
for (const i of icons) {
|
|
198
|
+
const list = bySet.get(i.s) ?? [];
|
|
199
|
+
const c = componentName(i);
|
|
200
|
+
if (!list.includes(c)) list.push(c);
|
|
201
|
+
bySet.set(i.s, list);
|
|
202
|
+
}
|
|
203
|
+
return [...bySet.entries()].map(([set, names]) => ({
|
|
204
|
+
set,
|
|
205
|
+
pkg: libPackage(set),
|
|
206
|
+
names,
|
|
207
|
+
statement: `import { ${names.join(", ")} } from '${libPackage(set)}'`
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
BRAND,
|
|
212
|
+
assignComponentNames,
|
|
213
|
+
buildSvg,
|
|
214
|
+
cliFlags,
|
|
215
|
+
componentName,
|
|
216
|
+
fileName,
|
|
217
|
+
hexToHsv,
|
|
218
|
+
hsvToHex,
|
|
219
|
+
importLines,
|
|
220
|
+
isLight,
|
|
221
|
+
libPackage,
|
|
222
|
+
nativeStrokeWidth,
|
|
223
|
+
normalizeHex,
|
|
224
|
+
propsString,
|
|
225
|
+
snippetsFor,
|
|
226
|
+
supportsStrokeWidth,
|
|
227
|
+
svgToJsx,
|
|
228
|
+
toPascalCase
|
|
229
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rdicons/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-free helpers behind rdicons: recolor icons, change stroke and size, convert SVG to JSX, export PNG.",
|
|
5
|
+
"keywords": ["icons", "svg", "svg-to-jsx", "svg-to-png", "icon-customizer", "rdicons"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "rdicons contributors",
|
|
8
|
+
"homepage": "https://rdicons.vimalbhatt.com/docs/core",
|
|
9
|
+
"repository": { "type": "git", "url": "git+https://github.com/DevHydeOut/rdicons-lib.git" },
|
|
10
|
+
"bugs": { "url": "https://github.com/DevHydeOut/rdicons-lib/issues" },
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
19
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
20
|
+
},
|
|
21
|
+
"./browser": {
|
|
22
|
+
"import": { "types": "./dist/browser.d.ts", "default": "./dist/browser.js" },
|
|
23
|
+
"require": { "types": "./dist/browser.d.cts", "default": "./dist/browser.cjs" }
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
28
|
+
"engines": { "node": ">=18" },
|
|
29
|
+
"publishConfig": { "access": "public" },
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
33
|
+
"prepack": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"dompurify": "^3.4.17"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"tsup": "^8.5.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|