Add multi-library icon picker and favicon support
Introduces a unified icon picker supporting Material, Lucide, Tabler, Phosphor, Remix, Bootstrap, Heroicons, Feather, Simple Icons, and Radix, with category navigation and search. Adds option to use site favicon for bookmarks, updates bookmark rendering to support SVG and image icons, and refines popup and icon grid UI. Includes new iconSets.js for icon set management and updates CSS/HTML for improved icon selection experience.
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"title": "Apple",
|
||||||
|
"url": "apple.com",
|
||||||
|
"icon": "data:image/svg+xml;utf8,%3Csvg%20role%3D%22img%22%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22color%3A%20white%3B%22%3E%3Ctitle%3EApple%3C%2Ftitle%3E%3Cpath%20d%3D%22M12.152%206.896c-.948%200-2.415-1.078-3.96-1.04-2.04.027-3.91%201.183-4.961%203.014-2.117%203.675-.546%209.103%201.519%2012.09%201.013%201.454%202.208%203.09%203.792%203.039%201.52-.065%202.09-.987%203.935-.987%201.831%200%202.35.987%203.96.948%201.637-.026%202.676-1.48%203.676-2.948%201.156-1.688%201.636-3.325%201.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04%202.48-4.494%202.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675%201.09-4.61%201.09zM15.53%203.83c.843-1.012%201.4-2.427%201.245-3.83-1.207.052-2.662.805-3.532%201.818-.78.896-1.454%202.338-1.273%203.714%201.338.104%202.715-.688%203.559-1.701%22%2F%3E%3C%2Fsvg%3E",
|
||||||
|
"iconSet": "simple"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
+1
-3
@@ -1,3 +1 @@
|
|||||||
[
|
[]
|
||||||
|
|
||||||
]
|
|
||||||
+77
-71
@@ -227,6 +227,51 @@ body, html {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Favicon image in bookmark tile */
|
||||||
|
.bookmark-favicon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0,0,0,0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SVG icons in picker grid */
|
||||||
|
.icon-item .grid-svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon category navigation */
|
||||||
|
.icon-categories-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin: 0.25rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
.icon-picker-layout { display:flex; gap:1rem; align-items:stretch; background:#f6f7f9; border:1px solid #e2e5ea; border-radius:14px; padding:0.85rem 0.85rem 0.85rem 0.75rem; box-shadow:inset 0 0 0 1px #ffffff, 0 2px 4px rgba(0,0,0,0.05); min-height:280px; max-height:320px; overflow:hidden; }
|
||||||
|
.icon-side-nav { width:200px; display:flex; flex-direction:column; gap:0.3rem; overflow-y:auto; padding:0.3rem; background:linear-gradient(180deg,#fff,#f4f5f7); border:1px solid #d9dde2; border-radius:10px; max-height:100%; }
|
||||||
|
.icon-main { flex:1; min-width:0; display:flex; flex-direction:column; }
|
||||||
|
.icon-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem; }
|
||||||
|
.icon-filter-label { font-size:0.875rem; color:#555; font-weight:600; }
|
||||||
|
.favicon-toggle { display:flex; align-items:center; gap:0.4rem; }
|
||||||
|
.favicon-checkbox { width:16px; height:16px; accent-color:var(--accent); }
|
||||||
|
.favicon-label { font-size:0.75rem; color:#666; cursor:pointer; user-select:none; }
|
||||||
|
.icon-filter { margin-bottom:0.5rem; background:#fff; border:1px solid #d4d9df; border-radius:8px; padding:0.5rem 0.7rem; font-size:0.8rem; }
|
||||||
|
.icon-filter:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 2px rgba(123,46,255,0.25); }
|
||||||
|
.icon-cat-btn { width:100%; text-align:left; padding:0.55rem 0.65rem 0.55rem 0.55rem; font-size:0.7rem; font-weight:600; letter-spacing:.5px; background:transparent; border:1px solid transparent; border-radius:8px; cursor:pointer; color:#4a4f55; display:flex; align-items:center; gap:0.55rem; transition: background .18s, color .18s, border-color .2s; position:relative; }
|
||||||
|
.icon-cat-btn .material-symbols-outlined { font-size:18px; width:30px; height:30px; background:#e9edf1; border:1px solid #d4d9df; border-radius:8px; flex-shrink:0; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 1px #fff inset; }
|
||||||
|
.icon-cat-btn::before { display:none; }
|
||||||
|
.icon-cat-btn:hover { background:#eef2f6; }
|
||||||
|
.icon-cat-btn.active { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:rgba(255,255,255,0.35); box-shadow:0 2px 6px -2px rgba(0,0,0,0.35); }
|
||||||
|
.icon-cat-btn.active .material-symbols-outlined { background:rgba(255,255,255,0.22); border-color:rgba(255,255,255,0.4); color:#fff; }
|
||||||
|
|
||||||
|
.icon-section-label { display:none; }
|
||||||
|
.icon-section-anchor { height:1px; width:100%; margin-top:4px; }
|
||||||
|
|
||||||
.bookmark-title {
|
.bookmark-title {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -245,6 +290,18 @@ body, html {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dynamic bookmark icon colors based on theme */
|
||||||
|
.bookmark .material-symbols-outlined {
|
||||||
|
color: var(--text, #E0E0E0) !important;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure dark theme compatibility - fallback rules */
|
||||||
|
body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
||||||
|
.bookmark .material-symbols-outlined[style*="color: white"] {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add button style */
|
/* Add button style */
|
||||||
.add-bookmark {
|
.add-bookmark {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -286,29 +343,10 @@ body, html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Popup inner as white Material card */
|
/* Popup inner as white Material card */
|
||||||
.popup-inner {
|
.popup-inner { display:flex; flex-direction:column; gap:1.1rem; color:#222; min-width:400px; background:#ffffff; border-radius:16px; box-shadow:0 12px 40px -8px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.06); padding:1.5rem 1.5rem 1.25rem; transition:transform .35s cubic-bezier(.16,.84,.44,1), opacity .35s; transform:translateY(14px) scale(.94); opacity:0; width:760px; max-width:90vw; max-height:85vh; overflow:hidden; }
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
color: #222222;
|
|
||||||
min-width: 320px;
|
|
||||||
/* existing styling */
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
||||||
padding: 1.5rem;
|
|
||||||
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
|
|
||||||
transform: translateY(-10px) scale(0.95);
|
|
||||||
opacity: 0;
|
|
||||||
width: 500px; /* make popup wider */
|
|
||||||
max-width: 90vw; /* keep it responsive on small screens */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* animate in when not hidden */
|
/* animate in when not hidden */
|
||||||
.popup:not(.hidden) .popup-inner {
|
.popup:not(.hidden) .popup-inner { transform:translateY(0) scale(1); opacity:1; }
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* dialog title */
|
/* dialog title */
|
||||||
.popup-inner h2 {
|
.popup-inner h2 {
|
||||||
@@ -347,34 +385,7 @@ body, html {
|
|||||||
box-shadow: 0 0 0 2px rgba(123,46,255,0.2);
|
box-shadow: 0 0 0 2px rgba(123,46,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* icon-grid container */
|
/* Removed earlier duplicate icon-grid + icon-item block (consolidated below) */
|
||||||
.icon-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0 -0.5rem 1rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: #fafafa;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* individual icon items */
|
|
||||||
.icon-item {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-item:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* action buttons container */
|
/* action buttons container */
|
||||||
.popup-buttons {
|
.popup-buttons {
|
||||||
@@ -422,26 +433,21 @@ body, html {
|
|||||||
--text: #E0E0E0;
|
--text: #E0E0E0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon grid styling */
|
/* Unified icon grid styling */
|
||||||
.icon-grid {
|
.icon-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(42px,1fr)); gap:5px; flex:1 1 auto; min-height:160px; max-height:260px; overflow-y:auto; overflow-x:hidden; padding:0.6rem 0.6rem 0.7rem; background:rgba(255,255,255,0.45); backdrop-filter:blur(6px); border:1px solid rgba(0,0,0,0.08); border-radius:10px; position:relative; scroll-behavior:smooth; }
|
||||||
display: grid;
|
.icon-main { flex:1; min-height:300px; }
|
||||||
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
|
.icon-item { cursor:pointer; padding:6px; border:1px solid rgba(255,255,255,0.06); border-radius:10px; text-align:center; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.05); transition: background .15s, transform .15s, box-shadow .2s; font-size:0.65rem; line-height:1; font-weight:500; position:relative; }
|
||||||
gap: 8px;
|
.icon-item::after { content:""; position:absolute; inset:0; border-radius:inherit; box-shadow:0 0 0 0 rgba(0,0,0,0); transition:box-shadow .25s; }
|
||||||
max-height: 200px;
|
.icon-item:hover { background:rgba(255,255,255,0.12); }
|
||||||
overflow-y: auto;
|
.icon-item:active { transform:scale(.92); }
|
||||||
margin-bottom: 8px;
|
.icon-item.selected { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:transparent; box-shadow:0 4px 10px -3px rgba(0,0,0,.6); }
|
||||||
}
|
.icon-item.selected::after { box-shadow:0 0 0 2px rgba(255,255,255,0.65); }
|
||||||
.icon-item {
|
|
||||||
cursor: pointer;
|
/* Icon set selector row */
|
||||||
padding: 4px;
|
.icon-set-row {
|
||||||
border: 1px solid transparent;
|
display: flex;
|
||||||
border-radius: 4px;
|
align-items: center;
|
||||||
text-align: center;
|
gap: 0.5rem;
|
||||||
}
|
margin-top: 0.75rem;
|
||||||
.icon-item:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.icon-item.selected {
|
|
||||||
border-color: #0078d4;
|
|
||||||
background: rgba(0, 120, 212, 0.1);
|
|
||||||
}
|
}
|
||||||
|
.icon-section-label { display:none; }
|
||||||
|
|||||||
+18
-3
@@ -7,6 +7,8 @@
|
|||||||
<link rel="stylesheet" href="home.css">
|
<link rel="stylesheet" href="home.css">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@3/fonts/remixicon.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
@@ -59,11 +61,24 @@
|
|||||||
<input type="url" id="urlInput" placeholder="https://example.com">
|
<input type="url" id="urlInput" placeholder="https://example.com">
|
||||||
|
|
||||||
<!-- Icon picker -->
|
<!-- Icon picker -->
|
||||||
<label for="iconFilter">Icon</label>
|
<div class="icon-picker-layout">
|
||||||
|
<nav class="icon-side-nav" id="iconCategoryNav" aria-label="Icon Categories">
|
||||||
|
<!-- Category nav buttons injected here -->
|
||||||
|
</nav>
|
||||||
|
<div class="icon-main">
|
||||||
|
<div class="icon-header">
|
||||||
|
<label for="iconFilter" class="icon-filter-label">Icon</label>
|
||||||
|
<div class="favicon-toggle">
|
||||||
|
<input type="checkbox" id="useFavicon" class="favicon-checkbox">
|
||||||
|
<label for="useFavicon" class="favicon-label">Use site favicon</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<input type="text" id="iconFilter" class="icon-filter"
|
<input type="text" id="iconFilter" class="icon-filter"
|
||||||
placeholder="Search for icon or enter emoji">
|
placeholder="Search for icon, enter emoji, or type 'favicon'">
|
||||||
<div id="iconGrid" class="icon-grid"></div>
|
<div id="iconGrid" class="icon-grid" tabindex="0" aria-label="Icon selection list"></div>
|
||||||
<input type="hidden" id="selectedIcon">
|
<input type="hidden" id="selectedIcon">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- action buttons -->
|
<!-- action buttons -->
|
||||||
<div class="popup-buttons">
|
<div class="popup-buttons">
|
||||||
|
|||||||
+315
-30
@@ -1,4 +1,5 @@
|
|||||||
import { icons as initialIcons, fetchAllIcons } from './icons.js';
|
import { icons as initialIcons, fetchAllIcons } from './icons.js';
|
||||||
|
import { iconSets } from './iconSets.js';
|
||||||
|
|
||||||
const bookmarkList = document.getElementById('bookmarkList');
|
const bookmarkList = document.getElementById('bookmarkList');
|
||||||
const titleInput = document.getElementById('titleInput');
|
const titleInput = document.getElementById('titleInput');
|
||||||
@@ -14,8 +15,28 @@ const searchEngineLogo = document.getElementById('searchEngineLogo');
|
|||||||
const iconFilter = document.getElementById('iconFilter');
|
const iconFilter = document.getElementById('iconFilter');
|
||||||
const iconGrid = document.getElementById('iconGrid');
|
const iconGrid = document.getElementById('iconGrid');
|
||||||
const selectedIconInput= document.getElementById('selectedIcon');
|
const selectedIconInput= document.getElementById('selectedIcon');
|
||||||
|
const iconCategoryNav = document.getElementById('iconCategoryNav');
|
||||||
|
const useFaviconCheckbox = document.getElementById('useFavicon');
|
||||||
let selectedIcon = initialIcons[0];
|
let selectedIcon = initialIcons[0];
|
||||||
let availableIcons = initialIcons;
|
let availableIcons = initialIcons;
|
||||||
|
let currentIconSetKey = 'material';
|
||||||
|
const loadedSetsCache = new Map(); // key -> array
|
||||||
|
let unifiedCatalog = []; // aggregated icons with categories
|
||||||
|
// Semantic icon categories (ordered) with predicate tests
|
||||||
|
const iconCategories = [
|
||||||
|
{ id: 'services', label: 'Services', test: (n, set) => set === 'simple' || /(github|gitlab|google|twitter|facebook|discord|slack|whatsapp|youtube|spotify|apple|microsoft|aws|azure|gcp|cloudflare|figma|notion|paypal|stripe|reddit|steam|xbox|playstation|nintendo|openai|vercel|netlify|docker|kubernetes)/.test(n), icon: 'cloud' },
|
||||||
|
{ id: 'settings', label: 'Settings', test: n => /(setting|settings|cog|gear|tools?|wrench|sliders?|command|preferences?)/.test(n), icon: 'settings' },
|
||||||
|
{ id: 'files', label: 'Files & Data', test: n => /(file|folder|archive|book|bookmark|save|upload|download|cloud|database|server)/.test(n), icon: 'folder' },
|
||||||
|
{ id: 'media', label: 'Media', test: n => /(camera|video|film|image|photo|music|play|pause|mic|microphone|volume|speaker)/.test(n), icon: 'video_camera_front' },
|
||||||
|
{ id: 'social', label: 'Social & Communication', test: n => /(chat|message|mail|envelope|phone|comment|share|rss)/.test(n), icon: 'chat' },
|
||||||
|
{ id: 'nav', label: 'Navigation', test: n => /(map|compass|globe|route|pin|location|world|earth)/.test(n), icon: 'explore' },
|
||||||
|
{ id: 'security', label: 'Security', test: n => /(lock|shield|key|alert|warning|info|question|bug)/.test(n), icon: 'security' },
|
||||||
|
{ id: 'commerce', label: 'Commerce', test: n => /(cart|shopping|wallet|credit|bank|price|tag|sale|bag|store|shop)/.test(n), icon: 'shopping_cart' },
|
||||||
|
{ id: 'status', label: 'Status', test: n => /(star|heart|award|trophy|badge|bell|notification)/.test(n), icon: 'star' },
|
||||||
|
{ id: 'food', label: 'Food', test: n => /(apple|cake|coffee|cookie|beer|wine|food|restaurant|cup|tea)/.test(n), icon: 'restaurant' },
|
||||||
|
{ id: 'devices', label: 'Devices', test: n => /(cpu|laptop|desktop|tablet|phone|smartphone|device|monitor|tv)/.test(n), icon: 'devices' },
|
||||||
|
{ id: 'other', label: 'Other', test: () => true, icon: 'more_horiz' }
|
||||||
|
];
|
||||||
|
|
||||||
const searchEngines = {
|
const searchEngines = {
|
||||||
google: 'https://www.google.com/search?q=',
|
google: 'https://www.google.com/search?q=',
|
||||||
@@ -66,10 +87,26 @@ function renderBookmarks() {
|
|||||||
box.className = 'bookmark';
|
box.className = 'bookmark';
|
||||||
|
|
||||||
// prepend icon
|
// prepend icon
|
||||||
const iconEl = document.createElement('span');
|
const iconVal = b.icon || 'bookmark';
|
||||||
iconEl.className = 'material-symbols-outlined';
|
let iconEl;
|
||||||
iconEl.textContent = b.icon || 'bookmark';
|
if (typeof iconVal === 'string' && /^(https?:|data:)/.test(iconVal)) {
|
||||||
|
// Treat as favicon/image URL
|
||||||
|
iconEl = document.createElement('img');
|
||||||
|
iconEl.src = iconVal;
|
||||||
|
iconEl.alt = 'favicon';
|
||||||
|
iconEl.className = 'bookmark-favicon';
|
||||||
|
iconEl.referrerPolicy = 'no-referrer';
|
||||||
|
// Apply filter for dark backgrounds to ensure visibility
|
||||||
|
if (isDarkBackground()) {
|
||||||
|
iconEl.style.filter = 'brightness(0) saturate(100%) invert(100%)';
|
||||||
|
}
|
||||||
box.appendChild(iconEl);
|
box.appendChild(iconEl);
|
||||||
|
} else {
|
||||||
|
iconEl = document.createElement('span');
|
||||||
|
iconEl.className = 'material-symbols-outlined';
|
||||||
|
iconEl.textContent = iconVal;
|
||||||
|
box.appendChild(iconEl);
|
||||||
|
}
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.className = 'bookmark-title';
|
label.className = 'bookmark-title';
|
||||||
@@ -115,63 +152,251 @@ function renderBookmarks() {
|
|||||||
|
|
||||||
// draw the icon‐grid, filtering by the search term
|
// draw the icon‐grid, filtering by the search term
|
||||||
function renderIconGrid(filter = '') {
|
function renderIconGrid(filter = '') {
|
||||||
|
const f = filter.toLowerCase();
|
||||||
iconGrid.innerHTML = '';
|
iconGrid.innerHTML = '';
|
||||||
availableIcons
|
const frag = document.createDocumentFragment();
|
||||||
.filter(name => name.includes(filter))
|
let lastCat = null;
|
||||||
.forEach(name => {
|
const filtered = unifiedCatalog.filter(e => !f || e.name.includes(f));
|
||||||
|
filtered.forEach(entry => {
|
||||||
|
if (entry.category !== lastCat) {
|
||||||
|
lastCat = entry.category;
|
||||||
|
const anchor = document.createElement('div');
|
||||||
|
anchor.className = 'icon-section-anchor';
|
||||||
|
anchor.id = `section-${entry.category}`;
|
||||||
|
frag.appendChild(anchor);
|
||||||
|
}
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = 'material-symbols-outlined icon-item';
|
span.className = 'icon-item';
|
||||||
span.textContent = name;
|
const def = iconSets[entry.set];
|
||||||
|
if (entry.set === 'material') {
|
||||||
|
span.classList.add('material-symbols-outlined');
|
||||||
|
span.textContent = entry.name;
|
||||||
|
} else if (def && def.fontClass) {
|
||||||
|
const i = document.createElement('i');
|
||||||
|
i.className = def.fontClass(entry.name);
|
||||||
|
span.appendChild(i);
|
||||||
|
} else if (entry.dataUrl) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = entry.dataUrl; img.alt = entry.name; img.className = 'grid-svg';
|
||||||
|
span.appendChild(img);
|
||||||
|
} else {
|
||||||
|
span.textContent = '…';
|
||||||
|
(async () => {
|
||||||
|
if (def && def.fetchIcon) {
|
||||||
|
const dataUrl = await def.fetchIcon(entry.name);
|
||||||
|
if (dataUrl) {
|
||||||
|
entry.dataUrl = dataUrl;
|
||||||
|
if (span.isConnected) {
|
||||||
|
span.textContent='';
|
||||||
|
const img=document.createElement('img');
|
||||||
|
img.src=dataUrl; img.alt=entry.name; img.className='grid-svg';
|
||||||
|
span.appendChild(img);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If SVG fetch fails, try font class or show truncated name
|
||||||
|
if (def.fontClass && span.isConnected) {
|
||||||
|
span.textContent='';
|
||||||
|
const i = document.createElement('i');
|
||||||
|
i.className = def.fontClass(entry.name);
|
||||||
|
span.appendChild(i);
|
||||||
|
} else {
|
||||||
|
span.textContent = entry.name.slice(0,3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No fetchIcon available, show name
|
||||||
|
span.textContent = entry.name.slice(0,3);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
span.onclick = () => {
|
span.onclick = () => {
|
||||||
const currentSelected = iconGrid.querySelector('.icon-item.selected');
|
const currentSelected = iconGrid.querySelector('.icon-item.selected');
|
||||||
if (currentSelected) {
|
if (currentSelected) currentSelected.classList.remove('selected');
|
||||||
currentSelected.classList.remove('selected');
|
|
||||||
}
|
|
||||||
span.classList.add('selected');
|
span.classList.add('selected');
|
||||||
selectedIcon = name;
|
selectedIcon = entry.name;
|
||||||
selectedIconInput.value = name;
|
selectedIconInput.value = entry.name;
|
||||||
|
selectedIconInput.dataset.iconSet = entry.set;
|
||||||
|
if (entry.dataUrl) selectedIconInput.dataset.dataUrl = entry.dataUrl; else delete selectedIconInput.dataset.dataUrl;
|
||||||
};
|
};
|
||||||
iconGrid.appendChild(span);
|
frag.appendChild(span);
|
||||||
});
|
});
|
||||||
const first = iconGrid.querySelector('.icon-item');
|
iconGrid.appendChild(frag);
|
||||||
if (first) first.click();
|
// Don't auto-select first icon to allow favicon usage
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter as the user types
|
// filter as the user types
|
||||||
iconFilter.addEventListener('input', () =>
|
iconFilter.addEventListener('input', () => renderIconGrid(iconFilter.value.trim()));
|
||||||
renderIconGrid(iconFilter.value.trim().toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
// initial render
|
// initial render
|
||||||
renderIconGrid();
|
renderIconGrid();
|
||||||
|
|
||||||
// Asynchronously fetch all icons and update the grid
|
// Asynchronously fetch all icons and update the grid
|
||||||
(async () => {
|
async function buildUnifiedCatalog() {
|
||||||
try {
|
const keys = Object.keys(iconSets);
|
||||||
const allIcons = await fetchAllIcons();
|
for (const k of keys) {
|
||||||
availableIcons = allIcons;
|
if (!loadedSetsCache.has(k)) {
|
||||||
// Re-render with the full list, preserving any filter text
|
try { loadedSetsCache.set(k, await iconSets[k].loader()); }
|
||||||
renderIconGrid(iconFilter.value.trim().toLowerCase());
|
catch(e) { console.warn('Icon set load failed', k, e); loadedSetsCache.set(k, []); }
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch all icons:', error);
|
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
const temp = [];
|
||||||
|
for (const k of keys) {
|
||||||
|
const arr = loadedSetsCache.get(k) || [];
|
||||||
|
for (const name of arr) {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
const category = iconCategories.find(c => c.test(lower, k)).id;
|
||||||
|
temp.push({ set: k, name, category });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// order by category then by name
|
||||||
|
unifiedCatalog = temp.sort((a,b)=> {
|
||||||
|
if (a.category === b.category) return a.name.localeCompare(b.name);
|
||||||
|
return iconCategories.findIndex(c=>c.id===a.category) - iconCategories.findIndex(c=>c.id===b.category);
|
||||||
|
});
|
||||||
|
buildCategoryNav();
|
||||||
|
renderIconGrid(iconFilter.value.trim());
|
||||||
|
}
|
||||||
|
buildUnifiedCatalog();
|
||||||
|
|
||||||
|
// --- Favicon resolution helpers ---
|
||||||
|
async function resolveFavicon(rawUrl) {
|
||||||
|
if (!rawUrl) return null;
|
||||||
|
let url = rawUrl.trim();
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
url = 'https://' + url; // assume https if protocol missing
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
// Prefer Google favicon service for simplicity & size; fall back to /favicon.ico
|
||||||
|
const googleService = `https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(u.origin)}`;
|
||||||
|
// We'll optimisticly use google service; optionally we could verify it loads, but browsers will handle 404 gracefully.
|
||||||
|
return googleService;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to detect if background is dark
|
||||||
|
function isDarkBackground() {
|
||||||
|
// For SVG color modification, check if we have a dark theme
|
||||||
|
const rootStyles = window.getComputedStyle(document.documentElement);
|
||||||
|
const bgVar = rootStyles.getPropertyValue('--bg').trim();
|
||||||
|
|
||||||
|
if (bgVar && bgVar.startsWith('#')) {
|
||||||
|
const hex = bgVar.slice(1);
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16);
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16);
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return luminance < 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: assume dark theme for this app
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
saveBookmarkBtn.onclick = async () => {
|
saveBookmarkBtn.onclick = async () => {
|
||||||
const title = titleInput.value.trim();
|
const title = titleInput.value.trim();
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
const icon = selectedIcon;
|
let icon = selectedIcon;
|
||||||
if (!title || !url) return;
|
if (!title || !url) return;
|
||||||
|
|
||||||
bookmarks.push({ title, url, icon });
|
// Check if user wants to use favicon via checkbox
|
||||||
|
const wantFavicon = useFaviconCheckbox.checked;
|
||||||
|
|
||||||
|
if (wantFavicon) {
|
||||||
|
try {
|
||||||
|
const faviconUrl = await resolveFavicon(url);
|
||||||
|
if (faviconUrl) icon = faviconUrl;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Favicon fetch failed, falling back to icon symbol:', e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use selected icon if available
|
||||||
|
const hasSelectedIcon = document.querySelector('.icon-item.selected');
|
||||||
|
if (hasSelectedIcon) {
|
||||||
|
if (selectedIconInput.dataset.iconSet && selectedIconInput.dataset.iconSet !== 'material') {
|
||||||
|
if (selectedIconInput.dataset.dataUrl) {
|
||||||
|
icon = selectedIconInput.dataset.dataUrl;
|
||||||
|
|
||||||
|
// For SVG icons, modify color based on background
|
||||||
|
if (icon.startsWith('data:image/svg+xml') && isDarkBackground()) {
|
||||||
|
try {
|
||||||
|
// Decode the SVG and modify its color
|
||||||
|
const svgData = decodeURIComponent(icon.split(',')[1]);
|
||||||
|
const modifiedSvg = svgData.replace(/fill="[^"]*"/g, 'fill="white"')
|
||||||
|
.replace(/stroke="[^"]*"/g, 'stroke="white"')
|
||||||
|
.replace(/<svg([^>]*)>/, '<svg$1 style="color: white;">');
|
||||||
|
icon = 'data:image/svg+xml;utf8,' + encodeURIComponent(modifiedSvg);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to modify SVG color:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const def = iconSets[selectedIconInput.dataset.iconSet];
|
||||||
|
if (def && def.fetchIcon) {
|
||||||
|
const dataUrl = await def.fetchIcon(selectedIcon);
|
||||||
|
if (dataUrl) {
|
||||||
|
icon = dataUrl;
|
||||||
|
|
||||||
|
// Apply same color modification for fetched SVGs
|
||||||
|
if (icon.startsWith('data:image/svg+xml') && isDarkBackground()) {
|
||||||
|
try {
|
||||||
|
const svgData = decodeURIComponent(icon.split(',')[1]);
|
||||||
|
const modifiedSvg = svgData.replace(/fill="[^"]*"/g, 'fill="white"')
|
||||||
|
.replace(/stroke="[^"]*"/g, 'stroke="white"')
|
||||||
|
.replace(/<svg([^>]*)>/, '<svg$1 style="color: white;">');
|
||||||
|
icon = 'data:image/svg+xml;utf8,' + encodeURIComponent(modifiedSvg);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to modify fetched SVG color:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For Material icons, just use the icon name - CSS will handle color
|
||||||
|
icon = selectedIcon;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No icon selected and no favicon requested, use default bookmark icon
|
||||||
|
icon = 'bookmark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarks.push({ title, url, icon, iconSet: selectedIconInput.dataset.iconSet || 'material' });
|
||||||
await saveBookmarks();
|
await saveBookmarks();
|
||||||
renderBookmarks();
|
renderBookmarks();
|
||||||
|
|
||||||
titleInput.value = '';
|
titleInput.value = '';
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
|
iconFilter.value = '';
|
||||||
|
useFaviconCheckbox.checked = false;
|
||||||
|
// Clear any selected icon
|
||||||
|
const selected = document.querySelector('.icon-item.selected');
|
||||||
|
if (selected) selected.classList.remove('selected');
|
||||||
addPopup.classList.add('hidden');
|
addPopup.classList.add('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Disable icon selection when favicon toggle is checked
|
||||||
|
useFaviconCheckbox.addEventListener('change', () => {
|
||||||
|
const iconItems = document.querySelectorAll('.icon-item');
|
||||||
|
if (useFaviconCheckbox.checked) {
|
||||||
|
iconItems.forEach(item => {
|
||||||
|
item.style.opacity = '0.5';
|
||||||
|
item.style.pointerEvents = 'none';
|
||||||
|
});
|
||||||
|
// Clear any selection
|
||||||
|
const selected = document.querySelector('.icon-item.selected');
|
||||||
|
if (selected) selected.classList.remove('selected');
|
||||||
|
} else {
|
||||||
|
iconItems.forEach(item => {
|
||||||
|
item.style.opacity = '';
|
||||||
|
item.style.pointerEvents = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cancelBtn.onclick = () => {
|
cancelBtn.onclick = () => {
|
||||||
addPopup.classList.add('hidden');
|
addPopup.classList.add('hidden');
|
||||||
};
|
};
|
||||||
@@ -226,8 +451,68 @@ searchInput.addEventListener('keydown', e => {
|
|||||||
if (e.key === 'Enter') searchBtn.click();
|
if (e.key === 'Enter') searchBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildCategoryNav() {
|
||||||
|
iconCategoryNav.innerHTML = '';
|
||||||
|
const usedCategories = [...new Set(unifiedCatalog.map(e=>e.category))];
|
||||||
|
iconCategories.filter(c=>usedCategories.includes(c.id)).forEach(cat => {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'icon-cat-btn';
|
||||||
|
|
||||||
|
// Create icon element
|
||||||
|
const iconSpan = document.createElement('span');
|
||||||
|
iconSpan.className = 'material-symbols-outlined';
|
||||||
|
iconSpan.textContent = cat.icon;
|
||||||
|
|
||||||
|
// Create text element
|
||||||
|
const textSpan = document.createElement('span');
|
||||||
|
textSpan.textContent = cat.label;
|
||||||
|
|
||||||
|
btn.appendChild(iconSpan);
|
||||||
|
btn.appendChild(textSpan);
|
||||||
|
|
||||||
|
btn.onclick = () => {
|
||||||
|
const target = document.getElementById(`section-${cat.id}`);
|
||||||
|
if (target) {
|
||||||
|
const top = target.offsetTop;
|
||||||
|
iconGrid.scrollTo({ top: top - 4, behavior: 'smooth' });
|
||||||
|
iconCategoryNav.querySelectorAll('.icon-cat-btn').forEach(b => b.classList.toggle('active', b === btn));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
iconCategoryNav.appendChild(btn);
|
||||||
|
});
|
||||||
|
setupSectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSectionObserver() {
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const id = entry.target.id.replace('section-','');
|
||||||
|
const cat = iconCategories.find(c=>c.id===id);
|
||||||
|
if (!cat) return;
|
||||||
|
iconCategoryNav.querySelectorAll('.icon-cat-btn').forEach(b => {
|
||||||
|
const isActive = b.querySelector('span:last-child').textContent === cat.label;
|
||||||
|
b.classList.toggle('active', isActive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { root: iconGrid, threshold: 0, rootMargin: '0px 0px -85% 0px' });
|
||||||
|
// Observe after grid populated
|
||||||
|
const watch = () => {
|
||||||
|
iconGrid.querySelectorAll('.icon-section-anchor').forEach(l => observer.observe(l));
|
||||||
|
};
|
||||||
|
// Re-run after each render
|
||||||
|
const origRender = renderIconGrid;
|
||||||
|
renderIconGrid = function(filter='') { origRender(filter); watch(); };
|
||||||
|
watch();
|
||||||
|
}
|
||||||
|
|
||||||
// Load and render bookmarks immediately
|
// Load and render bookmarks immediately
|
||||||
(async () => {
|
(async () => {
|
||||||
bookmarks = await loadBookmarks();
|
bookmarks = await loadBookmarks();
|
||||||
|
// Wait a bit for styles to load before rendering
|
||||||
|
setTimeout(() => {
|
||||||
renderBookmarks();
|
renderBookmarks();
|
||||||
|
}, 100);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// Unified icon set loaders with graceful fallbacks.
|
||||||
|
// Each loader returns an array of string icon names (NOT SVG markup) suitable for name-based selection.
|
||||||
|
// Some libraries don't have an easy metadata endpoint; we attempt a fetch and fall back to a small curated subset.
|
||||||
|
|
||||||
|
import { fetchAllIcons as fetchMaterialIcons, icons as materialFallback } from './icons.js';
|
||||||
|
|
||||||
|
async function attemptJSON(url, transform) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { cache: 'no-store' });
|
||||||
|
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||||
|
const data = await res.json();
|
||||||
|
return transform ? transform(data) : data;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[IconSets] Failed to fetch', url, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SVG helpers ---
|
||||||
|
async function attemptText(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { cache: 'force-cache' });
|
||||||
|
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||||
|
const txt = await res.text();
|
||||||
|
if (!/^<svg[\s\S]*<\/svg>$/i.test(txt.trim())) throw new Error('Not SVG');
|
||||||
|
return txt;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function svgToDataUrl(svg) {
|
||||||
|
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg.replace(/<script[\s\S]*?<\/script>/gi, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticFallbacks = {
|
||||||
|
lucide: ['activity','airplay','alarm-clock','align-center','anchor','apple','archive','arrow-big-up','at-sign','award','battery','bell','bluetooth','book','bookmark','briefcase','calendar','camera','cast','check','chevron-down','chrome','cloud','code','command','compass','cpu','database','download','edit','external-link','eye','file','folder','gamepad','globe','heart','help-circle','home','image','info','keyboard','layers','link','list','lock','mail','map','menu','mic','moon','music','package','pie-chart','play','plus','pocket','power','refresh-ccw','rss','save','scissors','search','settings','share','shield','smartphone','speaker','star','sun','tablet','tag','terminal','thumbs-up','trash','tv','twitter','upload','user','video','wifi','x','zap'],
|
||||||
|
tabler: ['activity','alarm','affiliate','anchor','api','app-window','apple','archive','armchair','arrow-down','at','award','backspace','ballon','battery','bell','bluetooth','bolt','book','bookmark','briefcase','browser','bug','building','calendar','camera','car','chart-area','chart-bar','chart-pie','chart-scatter','check','chevron-down','cloud','code','coffee','color-swatch','command','compass','cpu','credit-card','dashboard','database','device-desktop','device-mobile','dice','dna','download','drop','edit','file','filter','flag','flame','folder','gift','globe','grid','hash','headphones','heart','help','home','id','inbox','info-circle','key','keyboard','language','layers','layout','layout-grid','letter-a','link','lock','login','logout','mail','map','menu','message','microphone','mood-happy','moon','music','news','note','package','password','phone','photo','player-play','plug','plus','power','printer','puzzle','refresh','rocket','route','rss','school','search','server','settings','share','shield','smart-home','snowflake','sparkles','star','sun','switch','tag','thumb-up','tool','trash','trophy','typography','upload','user','video','wifi','world','x'],
|
||||||
|
phosphor: ['activity','airplane','anchor','apple-logo','archive','arrow-down','arrow-up','at','bag','bell','book','bookmark','bounding-box','briefcase','browser','bug','calendar','camera','car','check','clipboard','cloud','code','command','compass','cpu','credit-card','database','device-mobile','device-tablet','door','download','drop','envelope','eye','eyedropper','file','film-strip','flag','flame','folder','funnel','game-controller','gear','globe','hand','hash','headphones','heart','house','image','info','key','keyboard','leaf','link','lock','magnet','magnifying-glass','map-pin','microphone','moon','music-note','note','nut','package','paper-plane','paperclip','path','pen','phone','plug','plus','power','printer','question','rocket','rss','scissors','share','shield','shopping-cart','sketch-logo','smiley','sparkle','speaker-high','star','sun','swatches','tag','terminal','thumbs-up','toolbox','trash','trophy','tv','user','users','video-camera','wifi-high','x','yarn','youtube-logo','zap'],
|
||||||
|
remix: ['add','alarm','alert','anchor','apps','archive','arrow-down','arrow-right','arrow-up','at','award','bank','bar-chart','battery','bell','bluetooth','book','bookmark','briefcase','bug','building','calendar','camera','car','chat','chrome','clipboard','cloud','code','command','compass','copyleft','copyright','cpu','dashboard','database','delete-bin','device','dice','download','dribbble','drive','earth','edge','edit','facebook','file','filter','fire','flag','folder','gamepad','gift','github','gitlab','global','google','group','hard-drive','heart','home','image','inbox','instagram','keyboard','keynote','layout','links','list','lock','login','logout','mac','mail','map','menu','message','mic','moon','music','notification','paragraph','pause','phone','picture-in-picture','play','plug','price-tag','print','qr-code','question','reddit','refresh','restart','rocket','rss','scales','search','secure-payment','send','settings','share','shield','shopping-bag','slack','smartphone','sound-module','star','sun','t-box','tablet','tag','telegram','thumb-up','timer','tool','trophy','twitter','tv','upload','usb','user','video','visa','voicemail','volume-up','wallet','wifi','windows','xbox','youtube','zoom-in'],
|
||||||
|
bootstrap: ['alarm','android','apple','archive','arrow-down','arrow-up','arrow-left','arrow-right','at','award','backspace','badge-4k','bag','bank','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','brush','bug','calendar','camera','card-image','card-list','cart','chat','check','chevron-down','circle','cloud','code','command','compass','cpu','credit-card','database','device-hdd','device-ssd','display','download','droplet','earbuds','emoji-smile','envelope','exclamation','eye','facebook','file','filter','flag','folder','funnel','gear','gift','globe','google','graph-up','grid','hammer','hand-thumbs-up','hash','headphones','heart','house','image','info','instagram','joystick','keyboard','laptop','layers','layout-split','lightning','link','lock','mailbox','map','megaphone','menu-button','mic','moon','music-note','nut','palette','paperclip','patch-check','pen','pencil','people','phone','pin','play','plug','plus','power','printer','qr-code','question','rocket','rss','save','scissors','search','server','share','shield','shop','skip-forward','slack','speaker','speedometer','star','sun','tablet','tag','terminal','tools','trash','trophy','truck','twitch','twitter','type','ui-checks','upload','usb','vector-pen','wallet','whatsapp','wifi','windows','wrench','x','youtube'],
|
||||||
|
heroicons: ['academic-cap','adjustments-horizontal','adjustments-vertical','archive-box','arrow-down','arrow-up','arrow-right','arrow-left','at-symbol','backspace','banknotes','bars-2','bars-3','battery-100','beaker','bell','bookmark','briefcase','cake','calendar','camera','chart-bar','chat-bubble-bottom-center','chat-bubble-left','check','chevron-down','chip','circle-stack','cloud','code-bracket','cog','command-line','computer-desktop','cpu-chip','cube','currency-dollar','device-phone-mobile','device-tablet','document','document-text','ellipsis-horizontal','envelope','exclamation-circle','eye','film','finger-print','fire','flag','folder','gift','globe-alt','hand-thumb-up','heart','home','identification','inbox','information-circle','key','language','lifebuoy','light-bulb','link','lock-closed','magnifying-glass','map','megaphone','microphone','moon','musical-note','newspaper','paint-brush','paper-airplane','paper-clip','phone','photo','play','plus','power','printer','puzzle-piece','qr-code','question-mark-circle','rocket-launch','rss','scale','scissors','server','share','shield-check','sparkles','square-3-stack-3d','star','sun','swatch','tag','trophy','tv','user','users','video-camera','wallet','wifi','wrench','x-mark'],
|
||||||
|
feather: ['activity','airplay','alert-circle','alert-triangle','anchor','aperture','archive','at-sign','award','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','calendar','camera','cast','check','chevron-down','chrome','circle','clipboard','cloud','code','command','compass','cpu','database','download','droplet','edit','eye','facebook','file','film','filter','flag','folder','gift','git-branch','git-commit','git-merge','github','gitlab','globe','grid','hash','headphones','heart','help-circle','home','image','info','instagram','key','layers','layout','link','lock','mail','map','menu','mic','monitor','moon','music','package','paperclip','pause','pen-tool','phone','play','plus','pocket','power','printer','radio','refresh-ccw','refresh-cw','repeat','rewind','rss','save','scissors','search','send','server','settings','share','shield','shopping-bag','shopping-cart','shuffle','slack','smartphone','speaker','square','star','sun','tablet','tag','target','terminal','thumbs-up','tool','trash','trello','trending-up','triangle','truck','tv','twitter','type','umbrella','unlock','upload','user','users','video','voicemail','volume','watch','wifi','wind','x','zap'],
|
||||||
|
simple: ['github','gitlab','google','youtube','twitter','facebook','twitch','discord','spotify','apple','microsoft','android','linux','ubuntu','x','linkedin','npm','pypi','docker','kubernetes','aws','azure','gcp','cloudflare','figma','notion','slack','whatsapp','meta','paypal','stripe','reddit','snapchat','steam','xbox','playstation','nintendo','instagram','pinterest','soundcloud','openai','vercel','netlify','digitalocean'],
|
||||||
|
radix: ['activity-log','airplane','backpack','bell','bookmark','calendar','camera','card-stack','caret-down','caret-up','chat-bubble','chat-dots','check','chevron-down','chevron-left','chevron-right','chevron-up','clock','code','component-1','component-2','cookie','copy','cube','discord-logo','double-arrow-down','double-arrow-left','double-arrow-right','double-arrow-up','drag-handle-dots-2','envelope-closed','envelope-open','exclamation-triangle','external-link','eye-open','file','file-text','file-plus','gear','globe','heart','home','image','info-circled','keyboard','laptop','layers','link-1','link-2','lock-closed','magic-wand','magnifying-glass','moon','notebook','open-in-new-window','paper-plane','pencil-1','person','pie-chart','pin-left','pin-right','plus','question-mark-circled','reload','rocket','rows','scissors','share-1','share-2','shield','speaker-loud','star','sun','target','trash','upload','video','zoom-in','zoom-out']
|
||||||
|
};
|
||||||
|
|
||||||
|
export const iconSets = {
|
||||||
|
material: {
|
||||||
|
label: 'Material',
|
||||||
|
loader: async () => { try { return await fetchMaterialIcons(); } catch { return materialFallback; } },
|
||||||
|
fetchIcon: async () => null
|
||||||
|
},
|
||||||
|
lucide: {
|
||||||
|
label: 'Lucide',
|
||||||
|
loader: async () => {
|
||||||
|
const data = await attemptJSON('https://cdn.jsdelivr.net/npm/lucide@latest/dist/metadata.json', d => Object.keys(d));
|
||||||
|
return data && data.length ? data : staticFallbacks.lucide;
|
||||||
|
},
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${name}.svg`);
|
||||||
|
return svg ? svgToDataUrl(svg) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabler: {
|
||||||
|
label: 'Tabler',
|
||||||
|
loader: async () => {
|
||||||
|
const data = await attemptJSON('https://cdn.jsdelivr.net/gh/tabler/tabler-icons@latest/icons.json', d => d.map(o => o.name));
|
||||||
|
return data && data.length ? data : staticFallbacks.tabler;
|
||||||
|
},
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const urls = [
|
||||||
|
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/outline/${name}.svg`,
|
||||||
|
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/filled/${name}.svg`
|
||||||
|
];
|
||||||
|
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
phosphor: {
|
||||||
|
label: 'Phosphor',
|
||||||
|
loader: async () => staticFallbacks.phosphor,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const styles = ['regular','bold','duotone','fill','light','thin'];
|
||||||
|
for (const style of styles) {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/${style}/${name}.svg`);
|
||||||
|
if (svg) return svgToDataUrl(svg);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remix: {
|
||||||
|
label: 'Remix',
|
||||||
|
loader: async () => staticFallbacks.remix,
|
||||||
|
fetchIcon: async () => null,
|
||||||
|
fontClass: (name) => `ri-${name}-line` // use line style font sprite
|
||||||
|
},
|
||||||
|
bootstrap: {
|
||||||
|
label: 'Bootstrap',
|
||||||
|
loader: async () => staticFallbacks.bootstrap,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/icons/${name}.svg`);
|
||||||
|
return svg ? svgToDataUrl(svg) : null;
|
||||||
|
},
|
||||||
|
fontClass: (name) => `bi-${name}`
|
||||||
|
},
|
||||||
|
heroicons: {
|
||||||
|
label: 'Heroicons',
|
||||||
|
loader: async () => staticFallbacks.heroicons,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const urls = [
|
||||||
|
`https://cdn.jsdelivr.net/npm/heroicons@2/24/outline/${name}.svg`,
|
||||||
|
`https://cdn.jsdelivr.net/npm/heroicons@2/24/solid/${name}.svg`
|
||||||
|
];
|
||||||
|
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
feather: {
|
||||||
|
label: 'Feather',
|
||||||
|
loader: async () => staticFallbacks.feather,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/feather-icons@4/dist/icons/${name}.svg`);
|
||||||
|
return svg ? svgToDataUrl(svg) : null;
|
||||||
|
},
|
||||||
|
fontClass: (name) => `icon-${name}` // fallback for display
|
||||||
|
},
|
||||||
|
simple: {
|
||||||
|
label: 'Simple Icons',
|
||||||
|
loader: async () => staticFallbacks.simple,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${name}.svg`);
|
||||||
|
return svg ? svgToDataUrl(svg) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
radix: {
|
||||||
|
label: 'Radix',
|
||||||
|
loader: async () => staticFallbacks.radix,
|
||||||
|
fetchIcon: async (name) => {
|
||||||
|
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@radix-ui/icons@latest/icons/${name}.svg`);
|
||||||
|
return svg ? svgToDataUrl(svg) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility: get list of set keys + label for UI
|
||||||
|
export function listIconSets() {
|
||||||
|
return Object.entries(iconSets).map(([key, val]) => ({ key, label: val.label }));
|
||||||
|
}
|
||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
[
|
[
|
||||||
|
"https://discord.com/",
|
||||||
|
"https://nebula.zambazosmedia.group/"
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user