@stnd/icon 0.3.0 → 0.4.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/CHANGELOG.md +12 -0
- package/Icon.svelte +9 -0
- package/embedded.js +22 -0
- package/package.json +1 -1
- package/resolve.server.js +49 -37
- package/tokens.js +0 -1
package/CHANGELOG.md
CHANGED
package/Icon.svelte
CHANGED
|
@@ -55,6 +55,15 @@
|
|
|
55
55
|
})
|
|
56
56
|
.then((text) => {
|
|
57
57
|
if (!isCancelled) {
|
|
58
|
+
// An app with an SPA fallback answers a missing icon with its own
|
|
59
|
+
// index.html and a 200, so `res.ok` proves nothing. Injecting that
|
|
60
|
+
// page via {@html} drops its <link rel="stylesheet"> into the
|
|
61
|
+
// document — a second copy of the app CSS, last in tree order, that
|
|
62
|
+
// re-asserts the framework defaults over the active theme for as
|
|
63
|
+
// long as the icon is mounted (Reveal's photo menu, 2026-09-30).
|
|
64
|
+
if (!/^\s*(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(text)) {
|
|
65
|
+
throw new Error(`Not an SVG: ${currentSrc}`);
|
|
66
|
+
}
|
|
58
67
|
// Ensure SVG scales properly by injecting a viewBox if missing
|
|
59
68
|
if (!text.includes("viewBox=")) {
|
|
60
69
|
const widthMatch = text.match(/<svg[^>]*\swidth="([^"]+)"/);
|
package/embedded.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded / Custom SVG Icons
|
|
3
|
+
*
|
|
4
|
+
* These icons are bundled directly into JS memory so they are guaranteed to
|
|
5
|
+
* resolve instantly with 0 disk I/O and 0 external network requests, even in
|
|
6
|
+
* serverless runtimes (like Cloudflare Workers SSR) where Node.js filesystem
|
|
7
|
+
* access (`fs.readFile`) is unavailable and the icon is not in upstream Iconify.
|
|
8
|
+
*/
|
|
9
|
+
export const embeddedIcons = {
|
|
10
|
+
"ph:obsidian": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
11
|
+
<path d="M19.355 18.538a68.967 68.959 0 0 0 1.858-2.954.81.81 0 0 0-.062-.9c-.516-.685-1.504-2.075-2.042-3.362-.553-1.321-.636-3.375-.64-4.377a1.707 1.707 0 0 0-.358-1.05l-3.198-4.064a3.744 3.744 0 0 1-.076.543c-.106.503-.307 1.004-.536 1.5-.134.29-.29.6-.446.914l-.31.626c-.516 1.068-.997 2.227-1.132 3.59-.124 1.26.046 2.73.815 4.481.128.011.257.025.386.044a6.363 6.363 0 0 1 3.326 1.505c.916.79 1.744 1.922 2.415 3.5zM8.199 22.569c.073.012.146.02.22.02.78.024 2.095.092 3.16.29.87.16 2.593.64 4.01 1.055 1.083.316 2.198-.548 2.355-1.664.114-.814.33-1.735.725-2.58l-.01.005c-.67-1.87-1.522-3.078-2.416-3.849a5.295 5.295 0 0 0-2.778-1.257c-1.54-.216-2.952.19-3.84.45.532 2.218.368 4.829-1.425 7.531zM5.533 9.938c-.023.1-.056.197-.098.29L2.82 16.059a1.602 1.602 0 0 0 .313 1.772l4.116 4.24c2.103-3.101 1.796-6.02.836-8.3-.728-1.73-1.832-3.081-2.55-3.831zM9.32 14.01c.615-.183 1.606-.465 2.745-.534-.683-1.725-.848-3.233-.716-4.577.154-1.552.7-2.847 1.235-3.95.113-.235.223-.454.328-.664.149-.297.288-.577.419-.86.217-.47.379-.885.46-1.27.08-.38.08-.72-.014-1.043-.095-.325-.297-.675-.68-1.06a1.6 1.6 0 0 0-1.475.36l-4.95 4.452a1.602 1.602 0 0 0-.513.952l-.427 2.83c.672.59 2.328 2.316 3.335 4.711.09.21.175.43.253.653z"/>
|
|
12
|
+
</svg>`,
|
|
13
|
+
"simple-icons:obsidian": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
14
|
+
<path d="M19.355 18.538a68.967 68.959 0 0 0 1.858-2.954.81.81 0 0 0-.062-.9c-.516-.685-1.504-2.075-2.042-3.362-.553-1.321-.636-3.375-.64-4.377a1.707 1.707 0 0 0-.358-1.05l-3.198-4.064a3.744 3.744 0 0 1-.076.543c-.106.503-.307 1.004-.536 1.5-.134.29-.29.6-.446.914l-.31.626c-.516 1.068-.997 2.227-1.132 3.59-.124 1.26.046 2.73.815 4.481.128.011.257.025.386.044a6.363 6.363 0 0 1 3.326 1.505c.916.79 1.744 1.922 2.415 3.5zM8.199 22.569c.073.012.146.02.22.02.78.024 2.095.092 3.16.29.87.16 2.593.64 4.01 1.055 1.083.316 2.198-.548 2.355-1.664.114-.814.33-1.735.725-2.58l-.01.005c-.67-1.87-1.522-3.078-2.416-3.849a5.295 5.295 0 0 0-2.778-1.257c-1.54-.216-2.952.19-3.84.45.532 2.218.368 4.829-1.425 7.531zM5.533 9.938c-.023.1-.056.197-.098.29L2.82 16.059a1.602 1.602 0 0 0 .313 1.772l4.116 4.24c2.103-3.101 1.796-6.02.836-8.3-.728-1.73-1.832-3.081-2.55-3.831zM9.32 14.01c.615-.183 1.606-.465 2.745-.534-.683-1.725-.848-3.233-.716-4.577.154-1.552.7-2.847 1.235-3.95.113-.235.223-.454.328-.664.149-.297.288-.577.419-.86.217-.47.379-.885.46-1.27.08-.38.08-.72-.014-1.043-.095-.325-.297-.675-.68-1.06a1.6 1.6 0 0 0-1.475.36l-4.95 4.452a1.602 1.602 0 0 0-.513.952l-.427 2.83c.672.59 2.328 2.316 3.335 4.711.09.21.175.43.253.653z"/>
|
|
15
|
+
</svg>`,
|
|
16
|
+
"ph:obsidian-color": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
|
17
|
+
<path fill="#7C3AED" d="m6.919 14.595c.64-.19 1.673-.484 2.86-.556-.712-1.797-.884-3.37-.746-4.77.16-1.616.73-2.966 1.286-4.114.118-.245.232-.473.342-.692.155-.31.3-.6.436-.895.227-.49.395-.922.48-1.324.084-.395.084-.748-.015-1.087-.099-.339-.31-.704-.71-1.104-.522-.135-1.101-.016-1.535.374L4.18 5.066c-.287.259-.476.61-.534.992l-.444 2.947c.699.618 2.424 2.414 3.473 4.906.094.223.182.45.264.684z"/>
|
|
18
|
+
<path fill="#6D28D9" d="m2.973 10.351c-.024.104-.058.205-.102.303l-2.725 6.073c-.28.624-.15 1.355.325 1.846l4.289 4.416c2.19-3.23 1.87-6.27.87-8.646-.758-1.8-1.908-3.21-2.657-3.992z"/>
|
|
19
|
+
<path fill="#8B5CF6" d="m5.75 23.51c.076.011.152.019.229.021.813.024 2.182.095 3.292.3.905.166 2.7.668 4.178 1.1 1.127.33 2.288-.57 2.452-1.733.119-.848.343-1.808.755-2.687l-.01.003c-.698-1.947-1.586-3.204-2.517-4.007-.929-.8-1.928-1.174-2.894-1.31-1.604-.225-3.075.197-4.001.469.555 2.31.384 5.03-1.484 7.844z"/>
|
|
20
|
+
<path fill="#A78BFA" d="m17.37 19.31c.927-1.398 1.587-2.486 1.936-3.076.174-.295.142-.664-.064-.938-.538-.713-1.567-2.16-2.127-3.502-.577-1.378-.663-3.518-.668-4.561-.002-.396-.127-.783-.372-1.095l-3.331-4.232c-.012.19-.04.38-.08.568-.11.524-.32 1.045-.558 1.56-.14.303-.301.626-.465.953-.108.218-.218.437-.322.652-.538 1.113-1.04 2.32-1.18 3.74-.13 1.314.048 2.844.849 4.67.133.012.267.027.402.046 1.149.161 2.355.611 3.465 1.568.954.823 1.816 2.001 2.515 3.646z"/>
|
|
21
|
+
</svg>`,
|
|
22
|
+
};
|
package/package.json
CHANGED
package/resolve.server.js
CHANGED
|
@@ -3,6 +3,8 @@ import * as path from "path";
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import { getIconData, iconToSVG, iconToHTML, replaceIDs } from "@iconify/utils";
|
|
5
5
|
|
|
6
|
+
import { embeddedIcons } from "./embedded.js";
|
|
7
|
+
|
|
6
8
|
let __filename = "";
|
|
7
9
|
let __dirname = "";
|
|
8
10
|
try {
|
|
@@ -28,8 +30,9 @@ export const usedIcons = new Set();
|
|
|
28
30
|
*
|
|
29
31
|
* Resolution order:
|
|
30
32
|
* 1. In-memory cache (instant)
|
|
31
|
-
* 2.
|
|
32
|
-
* 3.
|
|
33
|
+
* 2. Embedded custom SVGs (offline in-memory, Cloudflare Workers SSR safe)
|
|
34
|
+
* 3. Local SVG file in packages/icon/icons/{prefix}/{name}.svg (offline, curated library)
|
|
35
|
+
* 4. Iconify API fetch (any icon set, last resort)
|
|
33
36
|
*
|
|
34
37
|
* @param {string} iconName Fully qualified icon name, e.g. "ph:house"
|
|
35
38
|
* @param {{ width?: string, height?: string }} [attrs]
|
|
@@ -53,6 +56,14 @@ export async function resolveIcon(iconName, attrs = {}) {
|
|
|
53
56
|
// Track every icon resolved — used by integration.js to copy only needed SVGs
|
|
54
57
|
usedIcons.add(iconName);
|
|
55
58
|
|
|
59
|
+
// 1. In-memory embedded icons (works in Cloudflare Workers SSR where fs.readFile throws)
|
|
60
|
+
if (embeddedIcons[iconName]) {
|
|
61
|
+
const svg = applyDimensions(embeddedIcons[iconName], width, height);
|
|
62
|
+
cache.set(cacheKey, svg);
|
|
63
|
+
return svg;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Local SVG file on disk (Node.js dev & SSG build)
|
|
56
67
|
let svg = await fromLocalFile(prefix, name, width, height);
|
|
57
68
|
if (!svg) svg = await fromApi(prefix, name, width, height);
|
|
58
69
|
|
|
@@ -60,46 +71,47 @@ export async function resolveIcon(iconName, attrs = {}) {
|
|
|
60
71
|
return svg;
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
// ── SVG Dimension & ViewBox Normalizer ───────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function applyDimensions(rawSvg, width, height) {
|
|
77
|
+
let svg = rawSvg;
|
|
78
|
+
|
|
79
|
+
// Ensure SVG scales properly by injecting a viewBox if missing (case-insensitive)
|
|
80
|
+
if (!/viewBox=/i.test(svg)) {
|
|
81
|
+
const widthMatch = svg.match(
|
|
82
|
+
/<svg[^>]*\swidth\s*=\s*["']?([^"'\s>]+)["']?/i,
|
|
83
|
+
);
|
|
84
|
+
const heightMatch = svg.match(
|
|
85
|
+
/<svg[^>]*\sheight\s*=\s*["']?([^"'\s>]+)["']?/i,
|
|
86
|
+
);
|
|
87
|
+
if (widthMatch && heightMatch) {
|
|
88
|
+
const w = parseFloat(widthMatch[1]);
|
|
89
|
+
const h = parseFloat(heightMatch[1]);
|
|
90
|
+
if (!isNaN(w) && !isNaN(h)) {
|
|
91
|
+
svg = svg.replace(/<svg\b/i, (m) => `${m} viewBox="0 0 ${w} ${h}"`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply requested dimensions to the <svg> element
|
|
97
|
+
svg = svg.replace(/<svg([^>]*)>/i, (_, attrs) => {
|
|
98
|
+
let updated = attrs
|
|
99
|
+
.replace(/\s*width\s*=\s*["']?[^"'\s>]+["']?/i, "")
|
|
100
|
+
.replace(/\s*height\s*=\s*["']?[^"'\s>]+["']?/i, "");
|
|
101
|
+
|
|
102
|
+
updated = updated.replace(/\s+/g, " ").trim();
|
|
103
|
+
return `<svg${updated ? " " + updated : ""} width="${width}" height="${height}">`;
|
|
104
|
+
});
|
|
105
|
+
return svg;
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
// ── Local SVG file resolver ──────────────────────────────────────────────────
|
|
64
109
|
|
|
65
110
|
async function fromLocalFile(prefix, name, width, height) {
|
|
66
111
|
const filePath = path.join(LOCAL_ICONS_DIR, prefix, `${name}.svg`);
|
|
67
112
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Ensure SVG scales properly by injecting a viewBox if missing (case-insensitive)
|
|
71
|
-
// This handles files that may include XML prologs, comments, or different quote styles.
|
|
72
|
-
if (!/viewBox=/i.test(svg)) {
|
|
73
|
-
const widthMatch = svg.match(
|
|
74
|
-
/<svg[^>]*\swidth\s*=\s*["']?([^"'\s>]+)["']?/i,
|
|
75
|
-
);
|
|
76
|
-
const heightMatch = svg.match(
|
|
77
|
-
/<svg[^>]*\sheight\s*=\s*["']?([^"'\s>]+)["']?/i,
|
|
78
|
-
);
|
|
79
|
-
if (widthMatch && heightMatch) {
|
|
80
|
-
const w = parseFloat(widthMatch[1]);
|
|
81
|
-
const h = parseFloat(heightMatch[1]);
|
|
82
|
-
if (!isNaN(w) && !isNaN(h)) {
|
|
83
|
-
// Insert viewBox after the <svg tag name. Use a case-insensitive match so it works for varied files.
|
|
84
|
-
svg = svg.replace(/<svg\b/i, (m) => `${m} viewBox="0 0 ${w} ${h}"`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Apply requested dimensions to the <svg> element
|
|
90
|
-
// Use a non-anchored, case-insensitive match so we find the opening <svg> even if there
|
|
91
|
-
// is an XML prolog or whitespace before it.
|
|
92
|
-
svg = svg.replace(/<svg([^>]*)>/i, (_, attrs) => {
|
|
93
|
-
// Replace or add width/height attributes (handle single or double quotes and optional spaces)
|
|
94
|
-
let updated = attrs
|
|
95
|
-
.replace(/\s*width\s*=\s*["']?[^"'\s>]+["']?/i, "")
|
|
96
|
-
.replace(/\s*height\s*=\s*["']?[^"'\s>]+["']?/i, "");
|
|
97
|
-
|
|
98
|
-
// Normalize spacing inside the tag to avoid accidental double spaces
|
|
99
|
-
updated = updated.replace(/\s+/g, " ").trim();
|
|
100
|
-
return `<svg${updated ? " " + updated : ""} width="${width}" height="${height}">`;
|
|
101
|
-
});
|
|
102
|
-
return svg;
|
|
113
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
114
|
+
return applyDimensions(raw, width, height);
|
|
103
115
|
} catch {
|
|
104
116
|
return null;
|
|
105
117
|
}
|