Repository: planetabhi/sargam-icons Branch: main Commit: 5ce9f8c56f55 Files: 20 Total size: 88.1 KB Directory structure: gitextract_hojju54v/ ├── .gitignore ├── .nvmrc ├── LICENSE.txt ├── README.md ├── functions/ │ └── _middleware.ts ├── package.json ├── public/ │ ├── .well-known/ │ │ ├── agent-skills/ │ │ │ ├── index.json │ │ │ └── sargam-icons/ │ │ │ └── SKILL.md │ │ ├── api-catalog │ │ └── mcp/ │ │ └── server-card.json │ ├── _headers │ ├── robots.txt │ └── sitemap.xml ├── rspack.config.ts ├── scripts/ │ └── generate-changelog.ts ├── src/ │ ├── fonts/ │ │ └── JivaMono.otf │ ├── index.ts │ ├── sargam.ts │ └── styles/ │ └── base.scss └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules dist src/assets src/template.html src/changelog.html src/changelog.json .env .DS_Store **/.DS_Store ================================================ FILE: .nvmrc ================================================ 22 ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2026 Abhimanyu Rana @planetabhi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, and/or publish copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ```ascii ____ ___ _____ __ ___ / __/__ _ / _ \___ / ___/__ _ / |/ /__ _ _\ \/ _ `/ / , _/ -_) / (_ / _ `/ / /|_/ / _ `/ /___/\_,_/ /_/|_|\__/ \___/\_,_/ /_/ /_/\_,_/ _______ ___ ________ __ ___ / __/ _ | / _ \ / ___/ _ | / |/ / _\ \/ __ | / , _/ / (_ / __ | / /|_/ / /___/_/ |_| /_/|_| \___/_/ |_| /_/ /_/ ``` ## Sargam Icons A collection of 1200+ handcrafted open-source icons for your exquisite designs. [[sargamicons.com]](https://sargamicons.com/) ♪♪♪ ヽ(ˇ∀ˇ )ゞ - Built using SVG stroke, providing maximum flexibility on styling. - Optimized vector paths and SVGs for better performance. - Request a new icon by creating an issue. [![jsDelivr downloads badge](https://data.jsdelivr.com/v1/package/npm/sargam-icons/badge)](https://www.jsdelivr.com/package/npm/sargam-icons) ================================================ FILE: functions/_middleware.ts ================================================ /** * Cloudflare Pages Function middleware for Markdown content negotiation. * * When a request includes `Accept: text/markdown`, this middleware fetches * the HTML response from the origin and converts it to a simplified Markdown * representation. Browsers and other clients still receive normal HTML. * * References: * - RFC 8288 (Web Linking) * - https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/ */ // Cloudflare Pages Function types (minimal inline definitions so the file // works without @cloudflare/workers-types installed locally). interface EventContext { request: Request; next: () => Promise; env: E; } type PagesFunction = (ctx: EventContext) => Promise | Response; interface Env {} function acceptsMarkdown(request: Request): boolean { const accept = request.headers.get("Accept") || ""; return accept.includes("text/markdown"); } /** Decode common HTML entities. */ function decodeEntities(text: string): string { return text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, " ") .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))); } /** Decode entities, collapse whitespace, and JSON-quote for safe front-matter. */ function sanitizeMeta(raw: string): string { const cleaned = decodeEntities(raw).replace(/\s+/g, " ").trim(); return JSON.stringify(cleaned); } /** Very small HTML-to-Markdown converter for static content pages. */ function htmlToMarkdown(html: string): string { let md = html; // Extract const titleMatch = md.match(/<title[^>]*>(.*?)<\/title>/is); const title = titleMatch ? titleMatch[1].trim() : ""; // Extract <meta name="description"> const descMatch = md.match( /<meta\s+name=["']description["']\s+content=["'](.*?)["']/is, ); const description = descMatch ? descMatch[1].trim() : ""; // Strip everything outside <body> const bodyMatch = md.match(/<body[^>]*>([\s\S]*)<\/body>/i); md = bodyMatch ? bodyMatch[1] : md; // Remove <script>, <style>, <nav>, <svg>, <noscript> blocks md = md.replace(/<(script|style|nav|svg|noscript)\b[\s\S]*?<\/\1>/gi, ""); // Convert headings md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n"); md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n"); md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n"); md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "\n#### $1\n"); // Convert links md = md.replace(/<a\s+[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)"); // Convert paragraphs and divs to line breaks md = md.replace(/<\/?(p|div|section|article|main|header|footer)\b[^>]*>/gi, "\n"); // Convert <br> tags md = md.replace(/<br\s*\/?>/gi, "\n"); // Convert <li> md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n"); // Convert <strong>/<b> and <em>/<i> md = md.replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**"); md = md.replace(/<(em|i)\b[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*"); // Convert <code> md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`"); // Strip all remaining HTML tags md = md.replace(/<[^>]+>/g, ""); // Decode common HTML entities md = decodeEntities(md); // Collapse excessive blank lines md = md.replace(/\n{3,}/g, "\n\n").trim(); // Prepend front-matter-style header const header = [ "---", title ? `title: ${sanitizeMeta(title)}` : null, description ? `description: ${sanitizeMeta(description)}` : null, "---", ] .filter(Boolean) .join("\n"); return `${header}\n\n${md}\n`; } /** Rough token estimate: ~1 token per 4 characters for English text. */ function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } export const onRequest: PagesFunction<Env> = async (context) => { if (!acceptsMarkdown(context.request)) { return context.next(); } // Fetch the original HTML response from the origin const response = await context.next(); const contentType = response.headers.get("Content-Type") || ""; if (!contentType.includes("text/html")) { return response; } const html = await response.text(); const markdown = htmlToMarkdown(html); const tokens = estimateTokens(markdown); const headers = new Headers(response.headers); headers.delete("Content-Length"); // body size changes headers.set("Content-Type", "text/markdown; charset=utf-8"); headers.set("Vary", "Accept"); headers.set("x-markdown-tokens", String(tokens)); return new Response(markdown, { status: response.status, headers, }); }; ================================================ FILE: package.json ================================================ { "name": "sargam-icons", "version": "1.6.7", "description": "A collection of 1200+ open-source icons.", "scripts": { "clean": "rimraf package *.tgz", "compress": "svgo -f ./src/assets/Duotone -o ./icons/Duotone && svgo -f ./src/assets/Fill -o ./icons/Fill && svgo -f ./src/assets/Line -o ./icons/Line", "generate-icons": "bun run clean && bun run compress", "generate-changelog": "bun run scripts/generate-changelog.ts", "generate-template": "bun run generate-changelog && bun run src/sargam.ts", "build": "bun run generate-template && bunx rspack build", "dev": "bun run generate-template && bunx rspack serve", "build:dev": "bun run generate-template && bunx rspack build --mode development", "preview": "bun run build && bunx serve dist" }, "repository": { "type": "git", "url": "git+https://github.com/planetabhi/sargam-icons.git" }, "files": [ "Icons/" ], "keywords": [ "icons", "line-icons", "fill-icons", "duotone-icons", "sargam-icons", "svg", "react", "optimized", "figma", "compressed", "sargam" ], "author": "@planetabhi", "license": "MIT", "bugs": { "url": "https://github.com/planetabhi/sargam-icons/issues" }, "homepage": "https://sargamicons.com/", "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.2", "@babel/preset-typescript": "^7.28.5", "@new-ui/colors": "^2.2.5", "@new-ui/reset": "^0.1.2", "@rspack/cli": "^2.0.1", "@rspack/core": "^2.0.1", "@rspack/dev-server": "^2.0.1", "@sargamdesign/colors": "^3.1.0", "@svgr/core": "^8.1.0", "@types/node": "^25.6.0", "babel-loader": "^10.1.1", "css-loader": "^7.1.4", "mini-css-extract-plugin": "^2.10.2", "rimraf": "^6.1.3", "sass-embedded": "^1.99.0", "sass-loader": "^16.0.7", "spacings": "^0.1.0", "svgo": "^4.0.1", "typescript": "^6.0.3" }, "main": "index.js" } ================================================ FILE: public/.well-known/agent-skills/index.json ================================================ { "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", "skills": [ { "name": "sargam-icons", "type": "skill-md", "description": "Discover and browse 1,200+ open-source icons in Line, Fill, and Duotone styles.", "url": "https://sargamicons.com/.well-known/agent-skills/sargam-icons/SKILL.md", "digest": "sha256:6d28bf89e3cfc983abf201ccb656f73efd4557046200c12c5a7896828db70d1d" } ] } ================================================ FILE: public/.well-known/agent-skills/sargam-icons/SKILL.md ================================================ # Sargam Icons Discovery Discover and browse 1,200+ open-source icons in Line, Fill, and Duotone styles from Sargam Icons. ## Usage - Browse icons at https://sargamicons.com/ - Download SVG icons from `/icons/Line/`, `/icons/Fill/`, `/icons/Duotone/` - View changelog at https://sargamicons.com/changelog.html - Source code at https://github.com/planetabhi/sargam-icons ## Icon Styles - **Line** — Outline/stroke icons - **Fill** — Solid filled icons - **Duotone** — Two-tone icons ## License MIT ================================================ FILE: public/.well-known/api-catalog ================================================ { "linkset": [ { "anchor": "https://sargamicons.com/", "service-doc": [ { "href": "https://github.com/planetabhi/sargam-icons", "type": "text/html" } ], "service-desc": [ { "href": "https://raw.githubusercontent.com/planetabhi/sargam-icons/main/README.md", "type": "text/markdown" } ], "describes": [ { "href": "https://sargamicons.com/" } ] } ] } ================================================ FILE: public/.well-known/mcp/server-card.json ================================================ { "serverInfo": { "name": "sargam-icons", "version": "1.6.7", "description": "A collection of 1,200+ open-source icons in Line, Fill, and Duotone styles." }, "capabilities": { "resources": true, "tools": false, "prompts": false }, "resources": [ { "name": "icons-line", "description": "Line-style SVG icons", "uri": "https://sargamicons.com/icons/Line/" }, { "name": "icons-fill", "description": "Fill-style SVG icons", "uri": "https://sargamicons.com/icons/Fill/" }, { "name": "icons-duotone", "description": "Duotone-style SVG icons", "uri": "https://sargamicons.com/icons/Duotone/" } ] } ================================================ FILE: public/_headers ================================================ # Link response headers for agent discovery (RFC 8288, RFC 9727) / Link: </sitemap.xml>; rel="sitemap"; type="application/xml" Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json" Link: <https://github.com/planetabhi/sargam-icons>; rel="service-doc" Vary: Accept # Serve api-catalog with correct Content-Type (RFC 9727) /.well-known/api-catalog Content-Type: application/linkset+json # MCP Server Card (SEP-1649) /.well-known/mcp/server-card.json Content-Type: application/json # Agent Skills Discovery index /.well-known/agent-skills/index.json Content-Type: application/json ================================================ FILE: public/robots.txt ================================================ # robots.txt for sargam-icons # https://www.rfc-editor.org/rfc/rfc9309 # Content Signals: https://contentsignals.org/ # Allow all crawlers full access User-agent: * Content-Signal: ai-train=yes, search=yes, ai-input=yes Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ # AI / LLM crawlers — allow indexing of public pages User-agent: GPTBot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: OAI-SearchBot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: ChatGPT-User Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: ClaudeBot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: Claude-Web Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: anthropic-ai Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: Google-Extended Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: Applebot-Extended Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: PerplexityBot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: Amazonbot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: Bytespider Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ User-agent: CCBot Allow: /icons/Line/ Allow: /icons/Fill/ Allow: /icons/Duotone/ Allow: / Disallow: /icons/ Sitemap: https://sargamicons.com/sitemap.xml ================================================ FILE: public/sitemap.xml ================================================ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://sargamicons.com/</loc> <lastmod>2026-04-18</lastmod> <priority>1.0</priority> </url> <url> <loc>https://sargamicons.com/changelog.html</loc> <lastmod>2026-04-18</lastmod> <priority>0.5</priority> </url> </urlset> ================================================ FILE: rspack.config.ts ================================================ import path from 'path'; import { rspack, Configuration } from '@rspack/core'; import { fileURLToPath } from 'url'; // Get __dirname equivalent in ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface BuildEnv { mode?: 'production' | 'development'; } interface Argv { mode?: 'production' | 'development'; } export default (env: BuildEnv, argv: Argv): Configuration => { const isProd = argv && argv.mode === 'production'; return { mode: isProd ? 'production' : 'development', entry: { bundle: path.resolve(__dirname, 'src/index.ts'), }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name][contenthash].js', clean: true, assetModuleFilename: '[name][ext]', }, devtool: isProd ? 'hidden-source-map' : 'eval-source-map', devServer: { static: { directory: path.resolve(__dirname, 'dist'), }, port: 3000, open: true, hot: true, compress: true, historyApiFallback: true, }, module: { rules: [ { test: /\.css$/, use: [rspack.CssExtractRspackPlugin.loader, 'css-loader'], }, { test: /\.s[ac]ss$/i, use: [ rspack.CssExtractRspackPlugin.loader, 'css-loader', { loader: 'sass-loader', options: { // Use package name string — ESM-safe, avoids require() in ESM context implementation: 'sass-embedded', }, }, ], }, { test: /\.[jt]s$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-typescript'], }, }, }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', }, ], }, resolve: { extensions: ['.ts', '.js', '.json'], }, plugins: [ new rspack.CopyRspackPlugin({ patterns: [ { from: path.resolve(__dirname, './Icons/Line'), to: 'icons/Line' }, { from: path.resolve(__dirname, './Icons/Duotone'), to: 'icons/Duotone' }, { from: path.resolve(__dirname, './Icons/Fill'), to: 'icons/Fill' }, { from: path.resolve(__dirname, './public/robots.txt'), to: 'robots.txt' }, { from: path.resolve(__dirname, './public/sitemap.xml'), to: 'sitemap.xml' }, { from: path.resolve(__dirname, './public/_headers'), to: '_headers' }, { from: path.resolve(__dirname, './public/.well-known'), to: '.well-known' }, ], }), new rspack.HtmlRspackPlugin({ title: 'Sargam Icons', filename: 'index.html', template: path.resolve(__dirname, 'src/template.html'), favicon: path.resolve(__dirname, 'src/favicon.ico'), meta: { viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no', }, }), new rspack.HtmlRspackPlugin({ title: 'Changelog - Sargam Icons', filename: 'changelog.html', template: path.resolve(__dirname, 'src/changelog.html'), favicon: path.resolve(__dirname, 'src/favicon.ico'), meta: { viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no', }, }), new rspack.CssExtractRspackPlugin({ filename: isProd ? '[name][contenthash].css' : '[name].css', }), ], }; }; ================================================ FILE: scripts/generate-changelog.ts ================================================ import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, '..'); interface ChangelogEntry { version: string; date: string; newIcons: string[]; highlights: string[]; } interface Changelog { generated: string; totalIcons: number; entries: ChangelogEntry[]; } function execGit(command: string): string { try { return execSync(command, { cwd: rootDir, encoding: 'utf-8' }).trim(); } catch { return ''; } } interface VersionCommit { hash: string; version: string; date: string; } function getVersionCommits(): VersionCommit[] { // Find commits with version patterns like "new:v1.6.7" or "new: v1.6.6" or just "v1.0.0" const output = execGit('git log --all --format="%H|%s|%ad" --date=short'); if (!output) return []; // Pattern to detect and capture version from commits like "new:v1.6.7", "new v1.6.6", "new: v1.0.0", or just "v1.0.0" // Uses optional colon (new:?) to match both "new:" and "new " const versionPattern = /^(?:new:?\s*)?v?(\d+\.\d+\.?\d*)/i; const versions: VersionCommit[] = []; const seenVersions = new Set<string>(); for (const line of output.split('\n')) { if (!line) continue; const [hash, subject, date] = line.split('|'); // Use single regex for both check and capture const match = subject.match(versionPattern); if (match) { let version = match[1]; // Normalize version (add .0 if needed) if (version.split('.').length === 2) { version += '.0'; } // Only take the first (most recent) commit for each version if (!seenVersions.has(version)) { seenVersions.add(version); versions.push({ hash, version, date }); } } } // Sort by version number descending versions.sort((a, b) => { const aParts = a.version.split('.').map(Number); const bParts = b.version.split('.').map(Number); for (let i = 0; i < 3; i++) { if ((bParts[i] || 0) !== (aParts[i] || 0)) { return (bParts[i] || 0) - (aParts[i] || 0); } } return 0; }); return versions; } function getNewIconsBetweenCommits(fromHash: string, toHash: string): string[] { // Get icons added between two commits const command = fromHash ? `git diff --name-status --diff-filter=A ${fromHash}..${toHash} -- "Icons/Line/*.svg"` : `git diff --name-status --diff-filter=A $(git rev-list --max-parents=0 HEAD)..${toHash} -- "Icons/Line/*.svg"`; const output = execGit(command); if (!output) return []; return output .split('\n') .filter(Boolean) .map((line) => { // Extract icon name from path like "A\tIcons/Line/si_IconName.svg" const match = line.match(/si_([^.]+)\.svg$/); return match ? match[1] : null; }) .filter((name): name is string => name !== null) .sort(); } function countTotalIcons(): number { const iconsDir = path.join(rootDir, 'Icons', 'Line'); try { const files = fs.readdirSync(iconsDir); return files.filter((f) => f.endsWith('.svg')).length; } catch { return 0; } } function generateChangelog(): Changelog { const versionCommits = getVersionCommits(); const entries: ChangelogEntry[] = []; // Limit to 20 most recent versions const maxVersions = 20; const versionsToProcess = versionCommits.slice(0, maxVersions); console.log(`Found ${versionCommits.length} version commits, processing ${versionsToProcess.length}`); for (let i = 0; i < versionsToProcess.length; i++) { const current = versionsToProcess[i]; // Look forward: from current version commit to next version commit (or HEAD for latest) const next = i > 0 ? versionsToProcess[i - 1] : null; // Get icons added AFTER this version commit up to the next version (or HEAD) const newIcons = getNewIconsBetweenCommits( current.hash, next?.hash || 'HEAD' ); console.log(`${current.version}: ${newIcons.length} new icons`); // Only add entries with new icons or if it's a major version if (newIcons.length > 0 || current.version.endsWith('.0')) { entries.push({ version: current.version, date: current.date, newIcons, highlights: [], }); } } return { generated: new Date().toISOString(), totalIcons: countTotalIcons(), entries, }; } // Generate and save changelog const changelog = generateChangelog(); const outputPath = path.join(rootDir, 'src', 'changelog.json'); fs.writeFileSync(outputPath, JSON.stringify(changelog, null, 2)); console.log(`\nChangelog generated successfully!`); console.log(`Output: ${outputPath}`); console.log(`Total versions: ${changelog.entries.length}`); console.log(`Total icons: ${changelog.totalIcons}`); ================================================ FILE: src/index.ts ================================================ import './styles/base.scss'; document.addEventListener('DOMContentLoaded', () => { const searchInput = document.getElementById('icon-search') as HTMLInputElement | null; const clearBtn = document.getElementById('icon-search-clear') as HTMLButtonElement | null; const grid = document.querySelector('#icon-grid .flex-grid') as HTMLElement | null; if (!searchInput || !grid) return; const items = Array.from(grid.querySelectorAll('.flex-grid-item')) as HTMLElement[]; function normalize(text: string): string { return (text || '').toLowerCase(); } function getItemName(item: HTMLElement): string { const name = item.getAttribute('data-icon-name'); return name || ''; } function filter(query: string): void { const q = normalize(query); if (!q) { items.forEach((el) => { el.style.display = ''; }); return; } items.forEach((el) => { const name = normalize(getItemName(el)); el.style.display = name.includes(q) ? '' : 'none'; }); } if (clearBtn) { clearBtn.hidden = (searchInput.value || '').length === 0; } filter(searchInput.value || ''); let frameRequested = false; function onInputLike(): void { const value = searchInput!.value; if (clearBtn) { clearBtn.hidden = value.length === 0; } if (frameRequested) return; frameRequested = true; requestAnimationFrame(() => { filter(value); frameRequested = false; }); } searchInput.addEventListener('input', onInputLike); searchInput.addEventListener('change', onInputLike); searchInput.addEventListener('search', onInputLike); if (clearBtn) { clearBtn.addEventListener('click', () => { searchInput.value = ''; clearBtn.hidden = true; filter(''); searchInput.focus(); }); } searchInput.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Escape') { searchInput.value = ''; if (clearBtn) clearBtn.hidden = true; filter(''); } }); // Cmd/Ctrl+K focuses the search input document.addEventListener('keydown', (e: KeyboardEvent) => { const isK = e.key === 'k' || e.key === 'K'; if (isK && (e.metaKey || e.ctrlKey)) { e.preventDefault(); searchInput.focus(); searchInput.select(); } }); }); ================================================ FILE: src/sargam.ts ================================================ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; // Get __dirname equivalent in ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ───────────────────────────────────────────── // Types // ───────────────────────────────────────────── interface ChangelogEntry { version: string; date: string; newIcons: string[]; highlights: string[]; } interface Changelog { generated: string; totalIcons: number; entries: ChangelogEntry[]; } // ───────────────────────────────────────────── // Helpers // ───────────────────────────────────────────── function getVersion(): string { try { const pkgPath = path.join(__dirname, '..', 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string }; return pkg.version ?? '1.6.7'; } catch { return '1.6.7'; } } function loadChangelog(): Changelog { try { const changelogPath = path.join(__dirname, 'changelog.json'); const data = fs.readFileSync(changelogPath, 'utf-8'); return JSON.parse(data) as Changelog; } catch { return { generated: '', totalIcons: 0, entries: [] }; } } function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit', }); } function getIconNames(directory: string): string[] { return fs .readdirSync(directory) .filter((file: string) => file.endsWith('.svg')) .map((file: string) => path.basename(file, '.svg')) .sort(); } // ───────────────────────────────────────────── // Shared HTML generators // ───────────────────────────────────────────── /** The brand logo SVG — shared between both pages. */ const BRAND_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g fill="var(--content-primary)" clip-path="url(#a)"><path fill-rule="evenodd" d="M29.163 16.038a5.965 5.965 0 0 0-3.234-3.972 4.934 4.934 0 0 0-2.13-.429.134.134 0 0 1-.12-.206c.38-.625.614-1.329.686-2.057a5.97 5.97 0 0 0-1.821-4.802 5.487 5.487 0 0 0-2.247-1.348c-1.474-.443-2.637-.216-3.268.635-.35.504-.543 1.1-.552 1.714a2.86 2.86 0 0 0 .453 1.846 2.174 2.174 0 0 0 1.509.881 2.134 2.134 0 0 0 1.642-.531 2.077 2.077 0 0 0 .687-1.499.593.593 0 0 0-.498-.607.566.566 0 0 0-.635.562.932.932 0 0 1-.318.686.99.99 0 0 1-.762.25 1.027 1.027 0 0 1-.71-.414 1.77 1.77 0 0 1-.246-1.129c.004-.393.123-.775.342-1.1.443-.594 1.465-.399 2.034-.227a4.36 4.36 0 0 1 1.78 1.073 4.886 4.886 0 0 1 1.486 3.865 4.189 4.189 0 0 1-2.172 3.156l-.027.02-.147.086a.343.343 0 0 1-.447-.089 5.691 5.691 0 0 0-8.917 0 .343.343 0 0 1-.442.09l-.148-.087-.027-.02A4.194 4.194 0 0 1 8.743 9.23a4.908 4.908 0 0 1 1.484-3.865 4.37 4.37 0 0 1 1.784-1.073c.57-.172 1.592-.371 2.034.226.22.325.34.708.343 1.101.046.393-.04.79-.243 1.129a1.03 1.03 0 0 1-.71.414.98.98 0 0 1-.765-.25.933.933 0 0 1-.32-.686.565.565 0 0 0-.634-.562.593.593 0 0 0-.497.606 2.081 2.081 0 0 0 1.451 1.94 2.168 2.168 0 0 0 2.397-.773c.358-.544.52-1.194.456-1.842a3.13 3.13 0 0 0-.552-1.715c-.634-.85-1.794-1.077-3.268-.634a5.466 5.466 0 0 0-2.247 1.348 5.96 5.96 0 0 0-1.821 4.801c.071.729.306 1.432.686 2.058a.134.134 0 0 1-.12.206 4.929 4.929 0 0 0-2.127.429 5.978 5.978 0 0 0-3.237 3.971 5.519 5.519 0 0 0-.045 2.62c.343 1.5 1.132 2.401 2.185 2.511.092.007.185.007.278 0 .52-.012 1.03-.149 1.488-.398a2.83 2.83 0 0 0 1.372-1.313 2.164 2.164 0 0 0 0-1.746 2.123 2.123 0 0 0-2.12-1.254 2.093 2.093 0 0 0-.806.242.593.593 0 0 0-.278.738.566.566 0 0 0 .803.267.929.929 0 0 1 .765-.075.988.988 0 0 1 .682.948c0 .141-.029.28-.085.41-.19.347-.49.62-.854.775-.338.198-.728.291-1.119.267-.737-.085-1.076-1.07-1.213-1.65a4.36 4.36 0 0 1 .04-2.077 4.877 4.877 0 0 1 2.607-3.221 4.191 4.191 0 0 1 3.814.302l.178.103a.343.343 0 0 1 .144.428 5.703 5.703 0 0 0 4.761 7.752v.398a.517.517 0 0 0 0 .102 4.181 4.181 0 0 1-1.647 3.458 4.901 4.901 0 0 1-4.094.655 4.384 4.384 0 0 1-1.828-1.03c-.436-.407-1.118-1.196-.824-1.875.175-.351.45-.642.789-.837a1.773 1.773 0 0 1 1.098-.343 1.028 1.028 0 0 1 .717.404.993.993 0 0 1 .008 1.147.944.944 0 0 1-.29.267.562.562 0 0 0-.144.86.593.593 0 0 0 .752.094 2.077 2.077 0 0 0 .946-1.349 2.122 2.122 0 0 0-.36-1.687 2.178 2.178 0 0 0-1.52-.864 2.822 2.822 0 0 0-1.82.528c-.53.313-.954.78-1.215 1.337-.422.974-.034 2.093 1.084 3.149a5.463 5.463 0 0 0 2.291 1.269 6.198 6.198 0 0 0 1.687.233 5.785 5.785 0 0 0 3.372-1.05 4.859 4.859 0 0 0 1.433-1.632.14.14 0 0 1 .193-.052c.021.012.04.03.051.052a4.87 4.87 0 0 0 1.437 1.633 5.769 5.769 0 0 0 3.369 1.049c.57 0 1.138-.078 1.686-.234a5.463 5.463 0 0 0 2.292-1.268c1.122-1.056 1.506-2.175 1.084-3.149a3.086 3.086 0 0 0-1.211-1.337 2.84 2.84 0 0 0-1.825-.528 2.163 2.163 0 0 0-1.516.864 2.11 2.11 0 0 0-.36 1.687 2.059 2.059 0 0 0 .947 1.348.593.593 0 0 0 .751-.093.568.568 0 0 0-.144-.86.92.92 0 0 1-.446-.628.982.982 0 0 1 .165-.786 1.027 1.027 0 0 1 .713-.404c.394-.012.78.109 1.098.343.34.195.616.486.792.837.292.686-.391 1.468-.823 1.876a4.396 4.396 0 0 1-1.821 1.005 4.902 4.902 0 0 1-4.092-.649 4.198 4.198 0 0 1-1.647-3.453v-.207a.343.343 0 0 1 .299-.342 5.713 5.713 0 0 0 4.85-5.615 5.652 5.652 0 0 0-.392-2.057.344.344 0 0 1 .145-.429l.181-.107a4.199 4.199 0 0 1 3.818-.302 4.892 4.892 0 0 1 2.603 3.221c.178.679.19 1.39.035 2.075-.138.58-.478 1.564-1.212 1.65a1.979 1.979 0 0 1-1.121-.268 1.755 1.755 0 0 1-.853-.775 1.029 1.029 0 0 1 0-.82.987.987 0 0 1 .596-.538.944.944 0 0 1 .717.048.617.617 0 0 0 .761-.096.562.562 0 0 0-.148-.857 2.084 2.084 0 0 0-1.68-.172 2.115 2.115 0 0 0-1.28 1.142 2.164 2.164 0 0 0 0 1.746 2.84 2.84 0 0 0 1.372 1.313 3.06 3.06 0 0 0 1.767.381c1.05-.12 1.828-1.029 2.181-2.51a5.487 5.487 0 0 0-.038-2.617ZM16 11.421a4.57 4.57 0 0 1 3.379 1.496.342.342 0 0 1-.086.531l-2.95 1.715a.685.685 0 0 1-.686 0l-2.95-1.715a.342.342 0 0 1-.082-.531A4.562 4.562 0 0 1 16 11.42Zm-4.579 4.589c.002-.465.074-.928.213-1.371a.342.342 0 0 1 .5-.192l2.957 1.714a.686.686 0 0 1 .343.594v3.409a.342.342 0 0 1-.419.343 4.585 4.585 0 0 1-3.594-4.497Zm9.158 0a4.582 4.582 0 0 1-3.591 4.459.343.343 0 0 1-.415-.343V16.72a.686.686 0 0 1 .343-.593l2.953-1.715a.344.344 0 0 1 .5.195c.143.454.214.928.21 1.403Z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M16 0C7.163 0 0 7.163 0 16s7.163 16 16 16 16-7.163 16-16S24.837 0 16 0ZM1.132 16C1.132 7.789 7.789 1.132 16 1.132S30.868 7.789 30.868 16 24.211 30.868 16 30.868 1.132 24.211 1.132 16Z" clip-rule="evenodd"/><path d="M4.727 8.981a1.029 1.029 0 1 1 1.144 1.711 1.029 1.029 0 0 1-1.144-1.71Zm21.974-.173a1.029 1.029 0 1 0 0 2.057 1.029 1.029 0 0 0 0-2.057ZM16 27.328a1.029 1.029 0 1 0 0 2.059 1.029 1.029 0 0 0 0-2.058Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h32v32H0z"/></clipPath></defs></svg>`; /** Theme-toggle button SVGs — shared between both pages. */ const THEME_TOGGLE_SVGS = ` <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.41 13.28C7.332 10.205 6.716 5.693 8.357 2c-1.23.41-2.256 1.23-3.281 2.256a10.4 10.4 0 0 0 0 14.768c4.102 4.102 10.46 3.897 14.562-.205 1.026-1.026 1.846-2.051 2.256-3.282-3.896 1.436-8.409.82-11.486-2.256"/> </svg> <svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" aria-hidden="true" style="display:none"> <g clip-path="url(#sun-clip)"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="M5 12H1m22 0h-4M7.05 7.05 4.222 4.222m15.556 15.556L16.95 16.95m-9.9 0-2.828 2.828M19.778 4.222 16.95 7.05M12 19v4m0-22v4m4 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0"/> </g> <defs> <clipPath id="sun-clip"><path fill="#fff" d="M0 0h24v24H0z"/></clipPath> </defs> </svg>`; /** The icon-popover dialog HTML — identical on both pages. */ function generatePopoverHtml(): string { return ` <div id="icon-popover" class="icon-popover" hidden role="dialog" aria-labelledby="popover-title" aria-modal="true"> <div class="popover-content"> <div class="popover-header"> <h3 id="popover-title" class="popover-icon-name"></h3> <button type="button" class="popover-close" aria-label="Close popover"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="m7.757 16.243 8.486-8.486m0 8.486L7.757 7.757"/></svg> </button> </div> <div class="popover-variants"> <button type="button" class="popover-variant active" data-variant="line">Line</button> <button type="button" class="popover-variant" data-variant="duotone">Duotone</button> <button type="button" class="popover-variant" data-variant="fill">Fill</button> </div> <div class="popover-preview"> <img class="popover-icon" src="" alt="" width="48" height="48"> </div> <div class="popover-menu"> <button type="button" class="popover-menu-item" id="copy-svg-btn"> <span>[ Copy SVG ]</span> </button> <button type="button" class="popover-menu-item" id="copy-cdn-btn"> <span>[ Copy CDN ]</span> </button> <button type="button" class="popover-menu-item" id="download-svg-btn"> <span>[ Download ]</span> </button> </div> </div> </div>`; } /** * The shared initIconPopover() script block. * * @param cdnBaseUrl - The CDN base URL injected at build time. * @param clickSelector - CSS selector for clickable icon elements. * @param showRandomOnLoad - Whether to open a random icon popover on page load. */ function generatePopoverScript( cdnBaseUrl: string, clickSelector: string, showRandomOnLoad: boolean, ): string { // cdnBaseUrl is interpolated as string literals into the generated JS — no runtime const needed return ` function initIconPopover() { const popover = document.getElementById('icon-popover'); const popoverContent = document.querySelector('.popover-content'); const popoverHeader = document.querySelector('.popover-header'); const popoverTitle = document.querySelector('.popover-icon-name'); const popoverIcon = document.querySelector('.popover-icon'); const popoverClose = document.querySelector('.popover-close'); const copyBtn = document.getElementById('copy-svg-btn'); const downloadBtn = document.getElementById('download-svg-btn'); const copyCdnBtn = document.getElementById('copy-cdn-btn'); const variantBtns = document.querySelectorAll('.popover-variant'); /** @type {{ iconName: string, iconType: string, iconUrl: string } | null} */ let currentIconData = null; let isDragging = false; let offsetX = 0; let offsetY = 0; let currentPosX = 0; let currentPosY = 0; let hasBeenPositioned = false; /** @type {Element | null} */ let previouslyFocusedElement = null; function getFocusableElements() { return popoverContent.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); } function trapFocus(e) { if (e.key !== 'Tab') return; const focusableElements = getFocusableElements(); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } function centerPopover() { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const popoverWidth = popoverContent.offsetWidth || 256; const popoverHeight = popoverContent.offsetHeight || 400; const offsetFromRight = 48; const offsetFromTop = 72; currentPosX = (viewportWidth / 2) - popoverWidth / 2 - offsetFromRight; currentPosY = (offsetFromTop + popoverHeight / 2) - (viewportHeight / 2); popoverContent.style.left = '50%'; popoverContent.style.top = '50%'; popoverContent.style.transform = 'translate(calc(-50% + ' + currentPosX + 'px), calc(-50% + ' + currentPosY + 'px))'; offsetX = 0; offsetY = 0; hasBeenPositioned = true; } function showPopover(iconElement) { const iconName = iconElement.getAttribute('data-name'); const iconType = iconElement.getAttribute('data-type') || 'line'; // For <img> elements, use their already-resolved src; for <button> elements (changelog) // .src is undefined so we fall back to constructing the URL from CDN + data attributes. const iconUrl = iconElement.src || ('${cdnBaseUrl}' + iconType.charAt(0).toUpperCase() + iconType.slice(1) + '/' + iconName + '.svg'); currentIconData = { iconName, iconType, iconUrl }; popoverTitle.textContent = iconName; popoverIcon.src = iconUrl; popoverIcon.alt = iconName + ' ' + iconType + ' icon'; variantBtns.forEach(function(btn) { btn.classList.toggle('active', btn.getAttribute('data-variant') === iconType); }); previouslyFocusedElement = document.activeElement; if (!hasBeenPositioned) { centerPopover(); } popover.hidden = false; popover.setAttribute('aria-hidden', 'false'); document.removeEventListener('keydown', trapFocus); document.addEventListener('keydown', trapFocus); setTimeout(function() { const focusable = getFocusableElements(); if (focusable.length > 0) focusable[0].focus(); }, 100); } function hidePopover() { popover.hidden = true; popover.setAttribute('aria-hidden', 'true'); currentIconData = null; isDragging = false; document.removeEventListener('keydown', trapFocus); if (previouslyFocusedElement && previouslyFocusedElement.focus) { previouslyFocusedElement.focus(); } previouslyFocusedElement = null; } function downloadIcon(iconName, iconType, iconUrl) { fetch(iconUrl) .then(function(response) { return response.blob(); }) .then(function(blob) { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = iconName + '-' + iconType + '.svg'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); }) .catch(function(error) { console.error('Failed to download the icon:', error); alert('Failed to download the icon. Please try again.'); }); } function copyTextToClipboard(text, btn) { const spanEl = btn ? btn.querySelector('span') : null; const originalText = spanEl ? spanEl.textContent : ''; function showCopied() { if (spanEl) { spanEl.textContent = '[ Copied ]'; setTimeout(function() { spanEl.textContent = originalText; }, 800); } } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(showCopied).catch(function(err) { console.error('Failed to copy:', err); }); } else { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showCopied(); } } function copyIconToClipboard(iconUrl) { fetch(iconUrl) .then(function(response) { return response.text(); }) .then(function(svgText) { copyTextToClipboard(svgText, copyBtn); }) .catch(function(error) { console.error('Failed to copy SVG:', error); alert('Failed to copy SVG. Please try again.'); }); } // Drag function dragStart(e) { if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; isDragging = true; popoverHeader.style.cursor = 'grabbing'; if (e.type === 'touchstart') { offsetX = e.touches[0].clientX - currentPosX; offsetY = e.touches[0].clientY - currentPosY; } else { offsetX = e.clientX - currentPosX; offsetY = e.clientY - currentPosY; } e.preventDefault(); } function drag(e) { if (!isDragging) return; e.preventDefault(); const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY; currentPosX = clientX - offsetX; currentPosY = clientY - offsetY; popoverContent.style.left = '50%'; popoverContent.style.top = '50%'; popoverContent.style.transform = 'translate(calc(-50% + ' + currentPosX + 'px), calc(-50% + ' + currentPosY + 'px))'; } function dragEnd() { if (isDragging) { isDragging = false; popoverHeader.style.cursor = ''; } } popoverHeader.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); popoverHeader.addEventListener('touchstart', dragStart, { passive: false }); document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('touchend', dragEnd); // Click handler document.querySelectorAll('${clickSelector}').forEach(function(el) { el.addEventListener('click', function(e) { e.stopPropagation(); showPopover(el); }); }); ${showRandomOnLoad ? ` // Show random icon on page load const allIcons = document.querySelectorAll('.downloadable-icon'); if (allIcons.length > 0) { const randomIcon = allIcons[Math.floor(Math.random() * allIcons.length)]; showPopover(randomIcon); } ` : ''} // Keyboard: open icon on grid item Enter/Space const flexGrid = document.querySelector('.flex-grid'); if (flexGrid) { flexGrid.addEventListener('keydown', function(e) { if (e.target.classList.contains('flex-grid-item')) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const firstIcon = e.target.querySelector('.downloadable-icon'); if (firstIcon) showPopover(firstIcon); } } }); } if (copyBtn) { copyBtn.addEventListener('click', function() { if (currentIconData) copyIconToClipboard(currentIconData.iconUrl); }); } if (downloadBtn) { downloadBtn.addEventListener('click', function() { if (currentIconData) downloadIcon(currentIconData.iconName, currentIconData.iconType, currentIconData.iconUrl); }); } if (popoverClose) popoverClose.addEventListener('click', hidePopover); document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && !popover.hidden) hidePopover(); }); variantBtns.forEach(function(btn) { btn.addEventListener('click', function() { if (!currentIconData) return; const variant = btn.getAttribute('data-variant'); const newUrl = '${cdnBaseUrl}' + variant.charAt(0).toUpperCase() + variant.slice(1) + '/' + currentIconData.iconName + '.svg'; currentIconData.iconType = variant; currentIconData.iconUrl = newUrl; popoverIcon.src = newUrl; popoverIcon.alt = currentIconData.iconName + ' ' + variant + ' icon'; variantBtns.forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); }); }); if (copyCdnBtn) { copyCdnBtn.addEventListener('click', function() { if (currentIconData) copyTextToClipboard(currentIconData.iconUrl, copyCdnBtn); }); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initIconPopover); } else { initIconPopover(); }`; } /** Shared theme-toggle IIFE — used on both pages. */ function generateThemeToggleScript(): string { return ` (function initThemeToggle() { var KEY = 'data-new-ui-theme'; var el = document.documentElement; var saved = localStorage.getItem(KEY); function apply(theme) { el.setAttribute('data-new-ui-theme', theme); } if (saved === 'dark--warm' || saved === 'light--warm') { apply(saved); } else { apply('dark--warm'); } var btn = document.getElementById('theme-toggle'); function syncIcons() { var isDark = (el.getAttribute('data-new-ui-theme') || 'dark--warm') === 'dark--warm'; var sun = document.getElementById('icon-sun'); var moon = document.getElementById('icon-moon'); if (sun && moon) { sun.style.display = isDark ? '' : 'none'; moon.style.display = isDark ? 'none' : ''; } if (btn) { btn.setAttribute('aria-pressed', isDark ? 'true' : 'false'); btn.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme'); btn.title = isDark ? 'Switch to light theme' : 'Switch to dark theme'; } } syncIcons(); if (btn) { btn.addEventListener('click', function() { var current = el.getAttribute('data-new-ui-theme') || 'dark--warm'; var next = current === 'light--warm' ? 'dark--warm' : 'light--warm'; apply(next); try { localStorage.setItem(KEY, next); } catch (e) {} syncIcons(); }); } })();`; } /** Progressive image loading with IntersectionObserver. */ function generateProgressiveLoadingScript(): string { return ` function initProgressiveLoading() { const images = document.querySelectorAll('.downloadable-icon'); function applyImage(img) { const tempImg = new Image(); tempImg.onload = function() { img.style.opacity = '1'; img.classList.add('loaded'); }; tempImg.onerror = function() { img.style.display = 'none'; const placeholder = img.nextElementSibling; if (placeholder && placeholder.classList.contains('icon-placeholder')) { placeholder.style.display = 'block'; } }; tempImg.src = img.src; } if ('IntersectionObserver' in window) { const imageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { applyImage(entry.target); observer.unobserve(entry.target); } }); }, { rootMargin: '50px 0px', threshold: 0.1 }); images.forEach(function(img) { imageObserver.observe(img); }); } else { images.forEach(applyImage); } }`; } /** Font-loading class management. */ function generateFontLoadingScript(): string { return ` function initFontLoading() { if (document.fonts && document.fonts.ready) { document.body.classList.add('font-loading'); document.fonts.ready.then(function() { document.body.classList.add('font-loaded'); document.body.classList.remove('font-loading'); }); setTimeout(function() { if (document.body.classList.contains('font-loading')) { document.body.classList.add('font-loaded'); document.body.classList.remove('font-loading'); } }, 3000); } }`; } /** Minimal critical CSS inlined in <head> to prevent FOUC before bundle loads. */ function getCriticalCSS(): string { return ` html, body { background-color: var(--background); margin: 0; } body { color: var(--content-primary); } main { width: 100%; margin: 0; padding: 0; } .top-nav { position: fixed; top: 0; width: 100%; z-index: 999; } header { width: 100%; margin: var(--s-112, 7rem) 0 var(--s-56, 3.5rem); text-align: center; } .flex-grid { display: flex; flex-wrap: wrap; } .flex-grid-item { flex: 1 0 4rem; display: flex; align-items: center; justify-content: center; position: relative; height: 9.5rem; } @media screen and (max-width: 639px) { header { margin: var(--s-128, 8rem) 0 var(--s-64, 4rem); } }`; } // ───────────────────────────────────────────── // Build data // ───────────────────────────────────────────── const VERSION = getVersion(); const CDN_BASE_URL = `https://cdn.jsdelivr.net/npm/sargam-icons@${VERSION}/Icons/`; const changelog = loadChangelog(); const iconNames = getIconNames(path.join(__dirname, '..', 'Icons', 'Line')); const criticalCSS = getCriticalCSS(); // ───────────────────────────────────────────── // Icon grid HTML // ───────────────────────────────────────────── let iconGridContent = ''; iconNames.forEach((iconName: string, index: number) => { iconGridContent += ` <div class="flex-grid-item" data-icon-name="${iconName}" tabindex="0" aria-label="${iconName} icon"> <img class="downloadable-icon" data-type="line" data-name="${iconName}" src="${CDN_BASE_URL}Line/${iconName}.svg" width="24" height="24" alt="${iconName} line style" loading="lazy" decoding="async" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> <div class="icon-placeholder" style="display:none; width:24px; height:24px; background:var(--border-muted); border-radius:2px; position:absolute; top:1.25rem; left:50%; transform:translateX(-50%);"></div> <img class="downloadable-icon" data-type="duotone" data-name="${iconName}" src="${CDN_BASE_URL}Duotone/${iconName}.svg" width="24" height="24" alt="${iconName} duotone style" loading="lazy" decoding="async" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> <div class="icon-placeholder" style="display:none; width:24px; height:24px; background:var(--border-muted); border-radius:2px; position:absolute; top:4rem; left:50%; transform:translateX(-50%);"></div> <img class="downloadable-icon" data-type="fill" data-name="${iconName}" src="${CDN_BASE_URL}Fill/${iconName}.svg" width="24" height="24" alt="${iconName} fill style" loading="lazy" decoding="async" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"> <div class="icon-placeholder" style="display:none; width:24px; height:24px; background:var(--border-muted); border-radius:2px; position:absolute; top:6.75rem; left:50%; transform:translateX(-50%);"></div> </div>`; }); // ───────────────────────────────────────────── // Main page (index / template.html) // ───────────────────────────────────────────── const fullHtmlContent = `<!DOCTYPE html> <html lang="en" data-new-ui-theme="dark--warm"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin> <link rel="dns-prefetch" href="https://cdn.jsdelivr.net"> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon"> <title>sargam icons

Handcrafted. Optimized. Open-source.

Sargam Icons

${iconGridContent}
${generatePopoverHtml()} `; fs.writeFileSync(path.join(__dirname, 'template.html'), fullHtmlContent); // ───────────────────────────────────────────── // Changelog page (changelog.html) // ───────────────────────────────────────────── const changelogEntriesHtml = changelog.entries .map((entry: ChangelogEntry, index: number) => { const isFirst = index === 0; const iconCount = entry.newIcons.length; const iconsHtml = entry.newIcons .map( (iconName: string) => ` `, ) .join(''); return `
v${entry.version} ${formatDate(entry.date)} ${iconCount}
${iconCount > 0 ? `
${iconsHtml}
` : '

No new icons in this version

'}
`; }) .join(''); const changelogPageHtml = ` Changelog - Sargam Icons

Changelog

Changelog

${changelogEntriesHtml}
${generatePopoverHtml()} `; fs.writeFileSync(path.join(__dirname, 'changelog.html'), changelogPageHtml); console.log('generated successfully'); ================================================ FILE: src/styles/base.scss ================================================ @use "@new-ui/reset"; @use "@new-ui/colors"; @font-face { font-family: "Jiva Mono"; src: url("../fonts/JivaMono.woff2") format("woff2"); font-weight: normal; font-style: normal; font-display: swap; } :root { font-size: 16px; --sargam: "Jiva Mono", ui-monospace, Menlo, Consolas, monospace; scroll-behavior: smooth; scroll-padding-top: 2rem; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; --container-max-width: 100%; --container-padding: var(--s-16); --container-padding-mobile: 1.5rem; --shadow-color: rgb(0 0 0 / 0.04); --elevated-shadow: 0 0 0 1.5px var(--border-muted), 0 1px 1px -0.5px var(--shadow-color), 0 3px 3px -1.5px var(--shadow-color); --s-00: 0rem; --s-01: .0625rem; --s-02: .125rem; --s-04: .25rem; --s-06: .375rem; --s-08: .5rem; --s-12: .75rem; --s-16: 1rem; --s-20: 1.25rem; --s-24: 1.5rem; --s-32: 2rem; --s-40: 2.5rem; --s-48: 3rem; --s-56: 3.5rem; --s-64: 4rem; --s-72: 4.5rem; --s-80: 5rem; --s-96: 6rem; --s-112: 7rem; --s-120: 7.5rem; --s-128: 8rem; --s-160: 10rem; --c-text: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' height='24' width='24'%3E%3Cpath fill='black' d='M9 4H11V5H9V4ZM12 6H11V5H12V6ZM13 6H12V18H11V19H9V20H11V19H12V18H13V19H14V20H16V19H14V18H13V6ZM14 5V6H13V5H14ZM14 5V4H16V5H14Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3Cpath fill='white' d='M15 3H16V4H15H14V3H15ZM14 5V4H13V5H12V4H11V3H10H9V4H8V5H9V6H10H11V18H10H9V19H8V20H9V21H10H11V20H12V19H13V20H14V21H15H16V20H17V19H16V18H15H14V6H15H16V5H17V4H16V5H15H14ZM13 6V5H14V6H13ZM13 18V6H12V5H11V4H10H9V5H10H11V6H12V18H11V19H10H9V20H10H11V19H12V18H13ZM13 18V19H14V20H15H16V19H15H14V18H13Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3C/svg%3E" ) 16 16, auto; --c-pointer: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 4H11V5H9V4ZM9 10V8V5H8V8V10H6V11H5V13H6V14H7V16H8V17H9V18H10V20H11H12H13H14V19H15H16V20H17V19H18V17H19V15H20V10H19V9H18H17V8H15H14V7H12V5H11V8V10H12V8H14V10H15V9H17V11H18V10H19V15H18V17H17V13H16V17H17V18V19H16V18H14V19H13H12H11V18H10V17H9V16H8V13H7H6V11H8V12H9V13H10V12V10H9ZM9 10H8V11H9V10ZM14 13H15V17H14V13ZM13 13H12V17H13V13Z' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M9 5H11V8V10H12V8H14V10H15V9H17V10V11H18V10H19V15H18V17H17V13H16V17H17V18V19H16V18H14V19H13H12H11V18H10V17H9V16H8V13H6V11H8V12H9V13H10V12V10H9V8V5ZM12 13V17H13V13H12ZM14 13V17H15V13H14Z' fill='white'/%3E%3C/svg%3E" ) 10 5, pointer; --c-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' height='24' width='24'%3E%3Cpath fill='black' d='M9 5H8V16H9V15H10V14H11V15H12V17H13V19H14H15V17H14V15H13V13H14H15H16V12H15V11H14V10H13V9H12V8H11V7H10V6H9V5Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3Cpath fill='white' d='M8 4H9V5H8V4ZM8 16H7V5H8V16ZM9 16V17H8V16H9ZM10 15V16H9V15H10ZM11 15H10V14H11V15ZM12 17H11V16V15H12V16V17ZM13 19H12V18V17H13V18V19ZM15 19V20H14H13V19H14H15ZM15 17H16V18V19H15V18V17ZM14 15H15V16V17H14V16V15ZM13 14V15H14V14H16V13H17V12H16V11H15V10H14V9H13V8H12V7H11V6H10V5H9V6H10V7H11V8H12V9H13V10H14V11H15V12H16V13H13V14Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3C/svg%3E" ) 12 12, text; --c-grab: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' height='24' width='24'%3E%3Cpath fill='black' d='M13 4H11V5H10V6H9V5H7V6H6V8H7V10H5V11H4V13H5V14H6V16H7V17H8V18H9V20H10H11H12H13V19H14H15V20H16V19H17V17H18V15H19V12H20V8H19V7H18V8H17V6H16V5H14H13V4ZM13 5V10H14V6H16V11H17V9H18V8H19V12H18V15H17V17H16V13H15V17H16V19H15V18H14H13V19H12H11H10V18H9V17H8V16H7V14H6V13H5V11H7V12H8V13H9V10H8V8H7V6H9V8H10V10H11V5H13ZM8 10V11H7V10H8ZM14 13H13V17H14V13ZM11 13H12V17H11V13Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3Cpath fill='white' d='M13 5H11V10H10V8H9V6H7V8H8V10H9V12V13H8V12H7V11H5V13H6V14H7V16H8V17H9V18H10V19H11H12H13V18H15V19H16V18V17H17V15H18V12H19V8H18V9H17V11H16V10V6H14V10H13V5ZM14 13H13V17H14V13ZM12 13H11V17H12V13ZM16 13H15V17H16V13Z' clip-rule='evenodd' fill-rule='evenodd'/%3E%3C/svg%3E") 12 12, grab; } html[data-new-ui-theme*="dark"] { --link: #E08742; --link-hover: #E9F37E; --link-visited: #E08742; --border-focus: var(--border); } html[data-new-ui-theme*="light"] { --link: #2972FF; --link-hover: var(--black); --link-visited: #2972FF; --border-focus: var(--border); } @mixin container-base { max-width: var(--container-max-width); width: 100%; margin-inline: auto; padding-inline: var(--container-padding); box-sizing: border-box; } // Shared styles for icon-style nav buttons (zoom-btn, theme-toggle) @mixin icon-btn { width: 32px; height: 32px; padding: 0; cursor: var(--c-pointer); display: inline-flex; align-items: center; justify-content: center; color: var(--content-secondary); background: none; border-left-color: var(--border-muted); border-top-color: var(--border-muted); border-right-color: var(--border); border-bottom-color: var(--border); border-style: solid; border-width: 1px; &:hover:not(:disabled) { color: var(--content-primary); background: var(--background-high-contrast); } &:focus { outline: 0; } svg { width: 16px; height: 16px; display: block; } } .container { @include container-base; } html, body { background-color: var(--background); cursor: var(--c-arrow); } body { color: var(--content-primary); font: 400 1em/1.5 var(--sargam); margin: var(--s-00); font-kerning: normal; overflow-wrap: break-word; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; &.font-loading { font-family: system-ui, sans-serif; } &.font-loaded { font-family: var(--sargam); } } p, h1, h2, h3, h4, h5, h6, li, pre, code, strong, dl, dt { cursor: var(--c-text); } a, a * { cursor: var(--c-pointer); } main { width: 100%; margin: 0; padding: 0; #icon-grid .flex-grid { @include container-base; } } img, svg { max-width: 100%; height: auto; aspect-ratio: 1 / 1; } #icon-grid { margin-bottom: var(--s-24); } :focus-visible { outline: 1.5px solid var(--border-focus); outline-offset: var(--s-02); box-shadow: none; } .flex-grid-item:focus, .flex-grid-item:focus-visible { outline: none; } .flex-grid-item:focus img, .flex-grid-item:focus-visible img { outline-offset: 2px; } .skip-link { position: absolute; top: -40px; left: 6px; background: var(--background-primary); color: var(--content-secondary); padding: var(--s-04); text-decoration: none; z-index: 1000; font-size: 0.875rem; &:focus { top: 6px; } } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } ::selection { background: var(--content-inked); color: var(--background); text-shadow: none; } a { color: var(--link); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; position: relative; text-transform: uppercase; &:hover { color: var(--link-hover); } &:visited { color: var(--link-visited); } } img, .flex-grid-item img[data-type="line"], .flex-grid-item img[data-type="duotone"], .flex-grid-item img[data-type="fill"] { position: relative; } header { width: 100%; margin: var(--s-112) 0 var(--s-56); text-align: center; padding: 0; .header-content { @include container-base; text-align: center; max-inline-size: 40rem; } h1 { font: 400 1.75rem/2.25rem var(--sargam); color: var(--content-primary); margin-bottom: var(--s-20); font-synthesis: none; text-rendering: optimizeLegibility; span { font: italic normal 1em var(--sargam); } } .CTAs { display: flex; justify-content: center; align-items: center; gap: var(--s-08); min-width: max-content; box-sizing: border-box; a { font: 400 0.875rem/1.563rem var(--sargam); padding: var(--s-02) var(--s-12); text-transform: uppercase; text-decoration-line: none; cursor: var(--c-pointer); color: var(--link); background: var(--background-high-contrast); border-left-color: var(--border-muted); border-top-color: var(--border-muted); border-right-color: var(--border-strong); border-bottom-color: var(--border-strong); border-style: solid; border-width: 1px; &:hover { color: var(--link-hover); } &:active { border-left-color: var(--border-strong); border-top-color: var(--border-strong); border-right-color: var(--border-muted); border-bottom-color: var(--border-muted); } } } } .flex-grid { display: flex; flex-wrap: wrap; &-item { flex: 1 0 calc(var(--s-64) * var(--zoom, 1)); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; margin: 0 calc(var(--s-02) * var(--zoom, 1)) calc(var(--s-04) * var(--zoom, 1)); height: calc(9.5rem * var(--zoom, 1)); background: var(--background-high-contrast); content-visibility: auto; contain-intrinsic-size: 0 calc(9.5rem * var(--zoom, 1)); border: 1.5px dotted var(--border-muted); img { &[data-type="line"], &[data-type="duotone"], &[data-type="fill"] { width: calc(24px * var(--zoom, 1)); height: calc(24px * var(--zoom, 1)); position: absolute; object-fit: contain; aspect-ratio: 1 / 1; cursor: var(--c-pointer); &:hover { background: var(--content-secondary-alt); color: var(--content-primary); } } &[data-type="line"] { top: calc(1.25rem * var(--zoom, 1)); } &[data-type="duotone"] { top: calc(4rem * var(--zoom, 1)); } &[data-type="fill"] { top: calc(6.75rem * var(--zoom, 1)); } } } } .top-nav { justify-content: flex-end; align-items: center; width: 100%; display: flex; z-index: 999; position: fixed; inset: 0% 0% auto; border-bottom: 1px solid var(--border-muted); background: var(--background); &-inner { max-width: var(--container-max-width); width: 100%; margin: 0 auto; padding: var(--s-08); box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: var(--s-06); } .lhs, .rhs { display: flex; align-items: center; gap: var(--s-06); } a { color: var(--content-secondary); text-decoration: none; font: 400 14px/1.5 var(--sargam); &:hover { text-decoration: none; color: var(--content-primary); } } .nav-search-wrapper { position: relative; margin-left: var(--s-00); input[type="search"] { width: 12rem; box-sizing: border-box; background-color: var(--background); box-shadow: none; appearance: none; border: 1px solid var(--border-muted); text-align: left; font: 400 0.875rem/1.5 var(--sargam); padding: var(--s-02) var(--s-06); color: var(--content-primary); caret-color: var(--content-primary); text-transform: uppercase; cursor: var(--c-text); --input-shadow: 2px 2px 0px 0px var(--background-secondary); box-shadow: var(--input-shadow) inset; &::placeholder { color: var(--content-placeholder); } &:focus { border-color: var(--border-focus); outline: 0; box-shadow: 0 0 0 .5px var(--border-focus); } &::-webkit-search-cancel-button { display: none; } } .clear-search { position: absolute; right: var(--s-04); top: 50%; transform: translateY(-50%); width: 24px; height: 24px; border: none; background: transparent; padding: 0; margin: 0; cursor: var(--c-pointer); color: var(--content-secondary-alt); display: inline-flex; align-items: center; justify-content: center; &:focus { outline: 0; box-shadow: 0 0 0 .5px var(--border-focus); } &[hidden] { display: none; } svg { width: 16px; height: 16px; } } } } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .brand { display: inline-flex; align-items: center; gap: 8px; svg { animation: rotate 4s linear infinite; animation-play-state: paused; } svg:hover, &:hover svg { animation-play-state: running; } } .version-pill { font: 400 14px/1 var(--sargam); padding: var(--s-02) var(--s-04); color: var(--content-primary); background: var(--background-selected); } .zoom-separator { width: 1px; height: 1rem; background: var(--border-muted); margin: 0 var(--s-04); } .zoom-btn { @include icon-btn; &:active:not(:disabled) { border-left-color: var(--border); border-top-color: var(--border); border-right-color: var(--border-muted); border-bottom-color: var(--border-muted); } &:disabled { opacity: 0.4; cursor: not-allowed; border: 1px solid transparent; } } .theme-toggle { @include icon-btn; &:active { border-left-color: var(--border); border-top-color: var(--border); border-right-color: var(--border-muted); border-bottom-color: var(--border-muted); } } .blank { height: 0; } footer { width: 100%; padding: 1rem 0; margin: 0; text-align: center; font: normal 0.875rem/1.5 var(--sargam); color: var(--content-secondary); border-top: 1px dotted var(--border-muted); .footer-content { @include container-base; } a { color: var(--link-hover); } } .icon-popover { position: fixed; inset: 0; z-index: 9999; pointer-events: none; &[hidden] { display: none; } &[hidden] .popover-content { opacity: 0; visibility: hidden; pointer-events: none; transition-delay: 0s; } } .popover-content { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; background: var(--background-high-contrast); width: 100%; max-width: 16rem; pointer-events: auto; border: 2px solid var(--black); visibility: visible; --ease-in: cubic-bezier(0.4, 0, 0.2, 1); transition-property: opacity, visibility; transition-duration: .2s; transition-timing-function: var(--ease-in); transition-delay: .2s; opacity: 1; --popover-shadow: 4px 4px 0px 0px var(--border); box-shadow: var(--popover-shadow); } .popover-header { display: flex; align-items: center; justify-content: space-between; padding: var(--s-00) var(--s-00) var(--s-00) var(--s-06); border-bottom: 2px solid var(--background-high-contrast); cursor: var(--c-grab); user-select: none; position: relative; z-index: 999; &:active { cursor: grabbing; } .popover-icon-name { font: 400 0.875rem/1.5 var(--sargam); color: var(--content-primary); margin: 0; text-transform: uppercase; cursor: var(--c-grab); max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .popover-close { width: 28px; height: 28px; padding: 0; margin-left: var(--s-08); cursor: var(--c-pointer); display: inline-flex; align-items: center; justify-content: center; color: var(--content-secondary); background: none; transition: transform 0.1s ease; border: none; &:hover { color: var(--content-primary); background: var(--background); } &:focus-visible { outline: 0; box-shadow: 0 0 0 .5px var(--border-focus); } &:active { transform: scale(0.97); } svg { width: 24px; height: 24px; display: block; } } } img.popover-icon { box-shadow: 0 0 0 1px var(--link-hover); } .popover-preview { display: flex; align-items: center; justify-content: center; padding: var(--s-32); background: var(--background); .popover-icon { width: 96px; height: 96px; object-fit: contain; } } .popover-preview:after { content: ""; position: absolute; inset: 0; z-index: 998; background: repeating-linear-gradient(0deg, var(--background-secondary) 0, var(--background-secondary) 2px, transparent 2px, transparent 4px); animation: lines 3s ease-out infinite; opacity: .2; mix-blend-mode: color-burn; pointer-events: none; } @keyframes lines { to { background-position: 0 25px } } .popover-menu { display: flex; flex-direction: column; gap: 0; border-top: .5px solid var(--border-muted); background: var(--background-high-contrast); position: relative; z-index: 999; } .popover-variants { display: flex; gap: 1px; position: relative; z-index: 999; background: var(--border-muted); border-top: 1px solid var(--border-muted); border-bottom: 1px solid var(--border-muted); } .popover-variant { flex: 1; padding: var(--s-06) var(--s-08); font: 400 0.75rem/1 var(--sargam); text-transform: uppercase; color: var(--content-secondary); background: var(--background); border: none; cursor: var(--c-pointer); transition: background 0.15s ease, color 0.15s ease; &:hover { background: var(--background-selected); } &.active { color: var(--content-primary); background: var(--background-selected); } } .popover-menu-item { display: flex; align-items: center; gap: var(--s-08); padding: var(--s-04); font: 400 0.875rem/1.5 var(--sargam); color: var(--content-secondary); background: none; border: none; border-bottom: .5px solid var(--border-muted); cursor: var(--c-pointer); text-align: left; text-transform: uppercase; transition: transform 0.1s ease; &:last-child { border-bottom: none; } &:hover { color: var(--link-hover); } &:focus { outline: 0; box-shadow: inset 0 0 0 .5px var(--border-focus); } &:active { transform: scale(0.98); } svg { width: 16px; height: 16px; flex-shrink: 0; } span { flex: 1; } } // ───────────────────────────────────────────── // Responsive overrides // ───────────────────────────────────────────── @media screen and (max-width: 639px) { :root { --container-padding: var(--container-padding-mobile); } header { margin: var(--s-128) 0 var(--s-64); } .top-nav-inner { margin: 0 auto 0; } .popover-content { left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; margin: var(--s-12); min-width: auto; } .popover-header { cursor: default !important; user-select: auto; &:active { cursor: default !important; } } .nav-search-wrapper { display: none; } .top-nav .lhs { gap: var(--s-04); } // Changelog responsive .changelog-summary { flex-wrap: wrap; gap: var(--s-06); } .changelog-count { width: 100%; } .changelog-icon-btn { width: 32px; height: 32px; } } // Changelog Section Styles .changelog-section { width: 100%; margin: var(--s-64) 0; border-top: 1px dotted var(--border-muted); padding-top: var(--s-48); } .changelog-page { padding-top: var(--s-64); } .changelog-page .changelog-section { border-top: none; margin-top: 0; padding-top: 0; } .changelog-header { text-align: center; margin: var(--s-48) 0 var(--s-32); h1 { font: 400 1.5rem/1.5 var(--sargam); color: var(--content-primary); margin: 0 0 var(--s-08); text-transform: uppercase; } p { font: 400 0.875rem/1.5 var(--sargam); color: var(--content-secondary); margin: 0; } } .changelog-empty { font: 400 0.875rem/1.5 var(--sargam); color: var(--content-secondary-alt); margin: 0; padding: var(--s-08); } .top-nav a.active { color: var(--link); text-decoration: underline; } .changelog-container { max-width: 32rem; width: 100%; margin: 0 auto; padding-inline: var(--container-padding); box-sizing: border-box; gap: 1px; display: flex; flex-direction: column; h2 { font: 400 1.25rem/1.5 var(--sargam); color: var(--content-primary); margin: 0 0 var(--s-04); text-transform: uppercase; text-align: center; } } .changelog-entry { background: var(--background-high-contrast); &[open] { .changelog-summary::after { transform: rotate(180deg); } } } .changelog-summary { display: flex; align-items: center; gap: var(--s-12); padding: var(--s-04) var(--s-12); cursor: var(--c-pointer); list-style: none; font: 400 14px/1 var(--sargam); text-transform: uppercase; &::-webkit-details-marker { display: none; } &:hover { background: var(--background-selected); } } .changelog-summary-rhs { display: flex; align-items: center; gap: var(--s-08); margin-left: auto; } .changelog-chevron { width: 20px; height: 20px; transition: transform 0.2s ease; color: var(--content-secondary); .changelog-entry[open] & { transform: rotate(90deg); } } .changelog-version { color: var(--link); font-weight: 400; min-width: 4rem; } .changelog-date { color: var(--content-secondary); } .changelog-count { display: inline-flex; align-items: center; justify-content: center; min-width: 1.5rem; padding: 0 var(--s-06); font: 400 14px/1 var(--sargam); color: var(--content-secondary); background: var(--background); border: 1px dotted var(--border-muted); } .changelog-content { padding: var(--s-16); border-top: 1px dotted var(--border-muted); background: var(--background); } .changelog-icons { display: flex; flex-wrap: wrap; gap: var(--s-06); align-items: center; } .changelog-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: var(--s-06); background: var(--background-high-contrast); border: 1px solid var(--border-muted); cursor: var(--c-pointer); transition: background 0.15s ease; overflow: hidden; &:hover { background: var(--background-selected); border-color: var(--border); } &:focus-visible { outline: 1.5px solid var(--border-focus); outline-offset: 1px; } img { width: 20px; height: 20px; object-fit: contain; &[alt] { font-size: 0; color: transparent; } } } [data-new-ui-theme="dark--warm"] { img[data-type="line"], img[data-type="duotone"], img[data-type="fill"], .popover-icon, .changelog-icon-btn img { filter: invert(1) sepia(100%) hue-rotate(180deg); } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "lib": [ "ES2022", "DOM", "DOM.Iterable" ], "moduleResolution": "bundler", "resolveJsonModule": true, "allowJs": true, "checkJs": false, "outDir": "./dist", "rootDir": "./", "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": false, "declarationMap": false, "sourceMap": false, "noEmit": true, "types": [ "node" ] }, "include": [ "src/**/*", "*.ts" ], "exclude": [ "node_modules", "dist", "Icons" ] }