UNPKG

iconforged

Version:

Frontend icon library. Bridging the gap between traditional icons and modern workflows.

352 lines (322 loc) 13.9 kB
(() => { const VERSION = '1.3.1'; const CDN = 'https://cdn.jsdelivr.net/npm/iconforged@latest/dist'; const FONT_NAME = 'IconForge'; const FONT_URL = `${CDN}/iconforge.woff2`; const META_ICONS_URL = `${CDN}/meta/iconforge-icons.json`; const META_STYLES_URL = `${CDN}/meta/iconforge-styles.json`; const usedClasses = new Set(); const injected = new Set(); let iconsMeta = {}; let stylesMeta = {}; const breakpoints = { 'xs:': '@media (min-width: 420px)', 'sm:': '@media (min-width: 640px)', 'md:': '@media (min-width: 768px)', 'lg:': '@media (min-width: 1024px)', 'xl:': '@media (min-width: 1280px)' }; const pseudoMap = { 'hover:': ':hover', 'focus:': ':focus', 'active:': ':active' }; const darkToken = 'dark:'; const allVariantTokens = [...Object.keys(breakpoints), ...Object.keys(pseudoMap), darkToken]; const arbitraryRE = /^(is-(?:color|bg|w|h|sq|size|p|pt|pr|pb|pl|px|py|m|mt|mr|mb|ml|my|mx|z|scale|opacity|rot|grid-cols|grid-rows|gap|top|bottom|left|right|translate|translate-x|translate-y|border|border-t|border-b|border-l|border-r|fixed-bg|gradient-linear|gradient-radial|gradient-conic|outline|outline-dashed|outline-dotted|outline-double|blur|backdrop-blur|brightness|contrast|grayscale|saturate))-\[(.+)\]$/; const escapeSelector = (s) => { return String(s).replace(/([ !\"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1'); }; const wrapMedia = (css, mediaQuery) => (mediaQuery ? `${mediaQuery}{${css}}` : css); const extractProps = (ruleStr) => { if (typeof ruleStr !== 'string') return ''; const m = ruleStr.match(/{([^}]+)}/); return m ? m[1].trim() : ''; }; function parseVariants(token) { let rest = String(token); let mediaQuery = ''; const pseudos = []; let darkPrefix = ''; outer: while (true) { for (const t of allVariantTokens) { if (rest.startsWith(t)) { if (t in breakpoints) { mediaQuery = breakpoints[t]; } else if (t === darkToken) { darkPrefix = '.dark '; } else if (t in pseudoMap) { pseudos.push(pseudoMap[t]); } rest = rest.slice(t.length); continue outer; } } break; } return { base: rest, mediaQuery, variantSel: pseudos.join(''), darkPrefix }; } function ruleForArbitrary(fullCls, type, valueRaw, variantSel, darkPrefix, isIconTarget) { const value = String(valueRaw); let props = ''; switch (type) { case 'is-color':props = `color: ${value};`;break; case 'is-bg':props = `background-color: ${value};`;break; case 'is-w':props = `width: ${value};`;break; case 'is-h':props = `height: ${value};`;break; case 'is-sq':props = `width: ${value}; height: ${value};`;break; case 'is-size':props = `font-size: ${value};`;break; case 'is-p':props = `padding: ${value};`;break; case 'is-pt':props = `padding-top: ${value};`;break; case 'is-pr':props = `padding-right: ${value};`;break; case 'is-pb':props = `padding-bottom: ${value};`;break; case 'is-pl':props = `padding-left: ${value};`;break; case 'is-px':props = `padding-left: ${value}; padding-right: ${value};`;break; case 'is-py':props = `padding-top: ${value}; padding-bottom: ${value};`;break; case 'is-m':props = `margin: ${value};`;break; case 'is-mt':props = `margin-top: ${value};`;break; case 'is-mr':props = `margin-right: ${value};`;break; case 'is-mb':props = `margin-bottom: ${value};`;break; case 'is-ml':props = `margin-left: ${value};`;break; case 'is-mx':props = `margin-left: ${value}; margin-right: ${value};`;break; case 'is-my':props = `margin-top: ${value}; margin-bottom: ${value};`;break; case 'is-z':props = `z-index: ${value};`; break; case 'is-scale':props = `transform: scale(${value});`; break; case 'is-opacity':props = `opacity: ${value};`;break; case 'is-rot':props = `transform: rotate(${value});`;break; case 'is-grid-cols':props = `grid-template-columns: repeat(${value}, minmax(0, 1fr));`;break; case 'is-grid-rows':props = `grid-template-rows: repeat(${value}, minmax(0, 1fr));`;break; case 'is-gap':props = `gap: ${value};`;break; case 'is-top':props = `top: ${value};`;break; case 'is-bottom':props = `bottom: ${value};`;break; case 'is-left':props = `left: ${value};`;break; case 'is-right':props = `right: ${value};`;break; case 'is-translate':props = `transform: translate(${value}, ${value});`;break; case 'is-translate-x':props = `transform: translateX(${value});`;break; case 'is-translate-y':props = `transform: translateY(${value});`;break; case 'is-border': props = `border-width: ${value} ${value} ${value} ${value}; border-style: solid;`;break; case 'is-border-t':props = `border-top-width: ${value}; border-style: solid;`;break; case 'is-border-b':props = `border-bottom-width: ${value}; border-style: solid;`;break; case 'is-border-l':props = `border-left-width: ${value}; border-style: solid;`;break; case 'is-border-r':props = `border-right-width: ${value}; border-style: solid;`;break; case 'is-outline':props = `outline: 1px solid ${value};`;break; case 'is-outline-dashed':props = `outline: 1px dashed ${value};`;break; case 'is-outline-dotted':props = `outline: 1px dotted ${value};`;break; case 'is-outline-double':props = `outline: 2px solid ${value};`;break; case 'is-blur':props = `filter: blur(${value});`;break; case 'is-backdrop-blur':props = `backdrop-filter: blur(${value});`;break; case 'is-brightness': props = `filter: brightness(${value});`;break; case 'is-contrast': props = `filter: contrast(${value});`;break; case 'is-grayscale': props = `filter: grayscale(${value});`;break; case 'is-saturate': props = `filter: saturate(${value});`;break; case 'is-fixed-bg': {const safeVal = value.replace(/"/g, '\\"');props = `position: fixed; top: 0; left: 0; width: 100dvw; height: 100dvh; z-index: -1; background-repeat: no-repeat; background-size: cover; background-image: url("${safeVal}");`;break;} case 'is-gradient-linear': {const safeVal = value.replace(/_/g, ' ').replace(/\"/g, '\\"');const ruleElem = `${darkPrefix}.${escapeSelector(fullCls)}${variantSel} { background-image: linear-gradient(${safeVal}); color: transparent; -webkit-background-clip: text; background-clip: text; }`;return `${ruleElem}`} case 'is-gradient-radial': {const safeVal = value.replace(/_/g, ' ').replace(/\"/g, '\\"');const ruleElem = `${darkPrefix}.${escapeSelector(fullCls)}${variantSel} { background-image: radial-gradient(${safeVal}); color: transparent; -webkit-background-clip: text; background-clip: text; }`;return `${ruleElem}`} case 'is-gradient-conic': {const safeVal = value.replace(/_/g, ' ').replace(/\"/g, '\\"');const ruleElem = `${darkPrefix}.${escapeSelector(fullCls)}${variantSel} { background-image: conic-gradient(${safeVal}); color: transparent; -webkit-background-clip: text; background-clip: text; }`;return `${ruleElem}`} default:props = ''; } if (!props) return ''; const selector = `${darkPrefix}.${escapeSelector(fullCls)}${variantSel}`; return `${selector} { ${props} }`; } const baseStyle = document.createElement('style'); baseStyle.textContent = ` /* IconForge CDN v${VERSION} */ @font-face { font-family: '${FONT_NAME}'; src: url('${FONT_URL}') format('woff2'); font-display: block; font-weight: normal; font-style: normal; } [class^="if-"], [class*=" if-"] { font-family: '${FONT_NAME}' !important; display: inline-block; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } `; document.head.appendChild(baseStyle); let dynamicStyle = document.querySelector('.iconforge-dynamic'); if (!dynamicStyle) { dynamicStyle = document.createElement('style'); dynamicStyle.classList.add('iconforge-dynamic'); baseStyle.after(dynamicStyle); } const fetchWithRetry = async (url, retries = 3) => { for (let i = 0; i < retries; i++) { try { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } catch (err) { if (i === retries - 1) throw err; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); } } }; const safeSave = (key, data) => { try { const s = JSON.stringify(data); if (s.length > 2_097_152) return false; sessionStorage.setItem(key, s); return true; } catch { return false; } }; const fetchMeta = async () => { const cachedIcons = sessionStorage.getItem('iconforge_icons'); const cachedStyles = sessionStorage.getItem('iconforge_styles'); if (cachedIcons && cachedStyles) { try { iconsMeta = JSON.parse(cachedIcons); stylesMeta = JSON.parse(cachedStyles); return; } catch { /* fall through */ } } const [icons, styles] = await Promise.all([ fetchWithRetry(META_ICONS_URL), fetchWithRetry(META_STYLES_URL) ]); iconsMeta = icons || {}; stylesMeta = styles || {}; safeSave('iconforge_icons', iconsMeta); safeSave('iconforge_styles', stylesMeta); }; function buildRuleForToken(token) { const { base, mediaQuery, variantSel, darkPrefix } = parseVariants(token); const arb = base.match(arbitraryRE); if (arb) { const [, type, valueRaw] = arb; const css = ruleForArbitrary(token, type, valueRaw, variantSel, darkPrefix, false); return { keyframes: '', css: wrapMedia(css, mediaQuery) }; } const styleDef = stylesMeta[base]; if (styleDef) { let keyframes = ''; let props = ''; if (typeof styleDef === 'object' && styleDef !== null) { if (styleDef.keyframes) keyframes = `${styleDef.keyframes}\n`; props = extractProps(styleDef.class) || styleDef.class || ''; } else if (typeof styleDef === 'string') { props = extractProps(styleDef) || styleDef; } if (props) { const sel = `.${escapeSelector(token)}${variantSel}`; const rule = `${darkPrefix}${sel} { ${props} }`; return { keyframes, css: wrapMedia(rule, mediaQuery) }; } return { keyframes, css: '' }; } const iconDef = iconsMeta[base]; if (iconDef) { let full = ''; if (typeof iconDef === 'object' && iconDef.value) full = iconDef.value; else if (typeof iconDef === 'string') full = iconDef; const props = extractProps(full) || full; if (props) { const sel = `.${escapeSelector(token)}${variantSel}:before`; const rule = `${darkPrefix}${sel} { ${props} }`; return { keyframes: '', css: wrapMedia(rule, mediaQuery) }; } } return { keyframes: '', css: '' }; } function tokenLooksRelevant(token) { if (!token) return false; if (token.startsWith('if-') || token.startsWith('is-')) return true; for (const t of allVariantTokens) { if (token.startsWith(t)) return true; } return false; } function scanElement(el) { const classes = el.className; if (typeof classes !== 'string') return; classes.split(/\s+/).forEach(token => { if (!tokenLooksRelevant(token)) return; const { base } = parseVariants(token); if (base && (base.startsWith('if-') || base.startsWith('is-'))) { usedClasses.add(token); } }); } function scanNode(root) { if (!root) return; if (root.nodeType === Node.ELEMENT_NODE) scanElement(root); const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); while (walker.nextNode()) scanElement(walker.currentNode); } function injectCollected() { let keyframesBuf = ''; let classesBuf = ''; usedClasses.forEach(token => { if (injected.has(token)) return; const { keyframes, css } = buildRuleForToken(token); if (keyframes && !injected.has(keyframes)) { keyframesBuf += keyframes; injected.add(keyframes); } if (css) { classesBuf += css + '\n'; injected.add(token); } }); const newCSS = keyframesBuf + classesBuf; if (newCSS) { requestAnimationFrame(() => { dynamicStyle.textContent += newCSS; }); } } (function hints() { try { const pre = document.createElement('link'); pre.rel = 'preconnect'; pre.href = new URL(CDN).origin; pre.crossOrigin = 'anonymous'; document.head.appendChild(pre); const preload = document.createElement('link'); preload.rel = 'preload'; preload.as = 'font'; preload.type = 'font/woff2'; preload.crossOrigin = 'anonymous'; preload.href = FONT_URL; document.head.appendChild(preload); } catch (e) { /* noop */ } })(); const run = async () => { await fetchMeta(); scanNode(document.documentElement); injectCollected(); let debounce; const observer = new MutationObserver((mutations) => { clearTimeout(debounce); debounce = setTimeout(() => { for (const m of mutations) { if (m.type === 'childList') { m.addedNodes.forEach(scanNode); } else if (m.type === 'attributes' && m.attributeName === 'class') { scanElement(m.target); } } injectCollected(); }, 16); }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } })();