A floating bubble to manage and quickly access all your important links, with extensive customization, backup, favicons, tags, folders, health checks, toast notifications, auto-categorize, and duplicate detection. Refined UI with sidebar folders and collapsible sections.
// ==UserScript==
// @name Universal Link Manager Pro
// @namespace http://tampermonkey.net/
// @version 5.0
// @description A floating bubble to manage and quickly access all your important links, with extensive customization, backup, favicons, tags, folders, health checks, toast notifications, auto-categorize, and duplicate detection. Refined UI with sidebar folders and collapsible sections.
// @author echoZ (Enhanced & Refined)
// @license MIT
// @match *://*/*
// @exclude *://routerlogin.net/
// @exclude *://192.168.1.1/
// @exclude *://192.168.0.1/
// @exclude *://my.bankofamerica.com/
// @exclude *://wellsfargo.com/
// @exclude *://chase.com/
// @exclude *://citibank.com/
// @exclude *://online.citi.com/
// @exclude *://capitalone.com/
// @exclude *://usbank.com/
// @exclude *://paypal.com/
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(async function() {
'use strict';
if (window.self !== window.top) return;
// --- SCRIPT EXCLUSION LOGIC ---
const excludedDomainsStorageKey = 'excludedUniversalDomains';
const currentUrl = window.location.href;
const excludedDomains = await GM_getValue(excludedDomainsStorageKey, []);
const isExcluded = excludedDomains.some(domain => currentUrl.includes(domain));
if (isExcluded) return;
// --- Storage Keys ---
const STORAGE_KEYS = {
links: 'universalLinkManagerLinks',
bubbleHidden: 'isBubbleHidden',
position: 'bubblePosition',
theme: 'universalLinkManagerTheme',
customColors: 'universalLinkManagerCustomColors',
categories: 'universalLinkManagerCategories',
settings: 'universalLinkManagerSettings',
excludedDomains: 'excludedUniversalDomains',
clickStats: 'universalLinkManagerClickStats',
folders: 'universalLinkManagerFolders'
};
// --- Default Data ---
const defaultLinks = [
{ label: 'Google', url: 'https://www.google.com/', category: 'default', shortcut: '', addedAt: Date.now() - 86400000 * 3, tags: [], folder: '', health: { status: 'unknown', checkedAt: 0 } },
{ label: 'Gemini AI', url: 'https://gemini.google.com/', category: 'default', shortcut: '', addedAt: Date.now() - 86400000 * 2, tags: [], folder: '', health: { status: 'unknown', checkedAt: 0 } },
{ label: 'OpenAI', url: 'https://www.openai.com/', category: 'default', shortcut: '', addedAt: Date.now() - 86400000, tags: [], folder: '', health: { status: 'unknown', checkedAt: 0 } }
];
const defaultCategories = [
{ name: 'default', color: '#888888' },
{ name: 'social', color: '#4287f5' },
{ name: 'work', color: '#2ecc71' },
{ name: 'entertainment', color: '#f542d4' },
{ name: 'tools', color: '#ff6b6b' }
];
const defaultSettings = {
bubbleIcon: 'λ',
bubbleSize: 60,
animationsEnabled: true,
openInNewTab: true,
showClickCount: false,
confirmDelete: true,
sortMode: 'manual',
recentCount: 10,
showFavicons: true,
defaultOpenSections: [],
sidebarCollapsed: false,
autoCategorizeDomains: {
'youtube.com': 'entertainment', 'netflix.com': 'entertainment', 'twitch.tv': 'entertainment', 'spotify.com': 'entertainment',
'twitter.com': 'social', 'x.com': 'social', 'facebook.com': 'social', 'instagram.com': 'social', 'reddit.com': 'social', 'linkedin.com': 'social', 'tiktok.com': 'social',
'github.com': 'tools', 'stackoverflow.com': 'tools', 'codepen.io': 'tools',
'docs.google.com': 'work', 'drive.google.com': 'work', 'notion.so': 'work', 'slack.com': 'work', 'trello.com': 'work'
}
};
const defaultCustomColors = {
bubbleBackground: '#0ff',
bubbleText: '#001f3f',
bubbleGlow: '#0ff',
menuBackground: '#222',
menuBorder: '#0ff',
linkBackground: '#333',
linkText: '#fff',
linkHover: '#0ff',
buttonBackground: '#444',
buttonText: '#0ff',
buttonHover: '#0ff'
};
// --- State Variables ---
let isDeleteMode = false;
let isExcludeDeleteMode = false;
let currentCategory = 'all';
let searchQuery = '';
let draggedItem = null;
let activeLetter = null;
let currentSortMode = 'manual';
let showRecentlyAdded = false;
let currentFolder = '';
let openAccordion = '';
// --- Data Management Functions ---
async function getData(key, defaultValue) {
return await GM_getValue(key, defaultValue);
}
async function setData(key, value) {
await GM_setValue(key, value);
}
async function getLinks() {
const links = await getData(STORAGE_KEYS.links, defaultLinks);
return links.map(link => ({
...link,
category: link.category || 'default',
shortcut: link.shortcut || '',
addedAt: link.addedAt || 0,
tags: link.tags || [],
folder: link.folder || '',
health: link.health || { status: 'unknown', checkedAt: 0 }
}));
}
async function saveLinks(links) {
await setData(STORAGE_KEYS.links, links);
}
async function getCategories() {
const saved = await getData(STORAGE_KEYS.categories, null);
if (!saved) {
await saveCategories(defaultCategories);
return defaultCategories;
}
if (saved.length > 0 && typeof saved[0] === 'string') {
const migrated = saved.map((name, index) => ({
name: name,
color: defaultCategories[index]?.color || generateRandomColor()
}));
await saveCategories(migrated);
return migrated;
}
return saved;
}
async function saveCategories(categories) {
await setData(STORAGE_KEYS.categories, categories);
}
function generateRandomColor() {
const colors = ['#4287f5', '#2ecc71', '#f542d4', '#ff6b6b', '#ffa500', '#9b59b6', '#1abc9c', '#e74c3c'];
return colors[Math.floor(Math.random() * colors.length)];
}
async function getSettings() {
const saved = await getData(STORAGE_KEYS.settings, {});
const settings = { ...defaultSettings, ...saved };
settings.autoCategorizeDomains = { ...defaultSettings.autoCategorizeDomains, ...(saved.autoCategorizeDomains || {}) };
if (!Array.isArray(settings.defaultOpenSections)) settings.defaultOpenSections = [];
if (typeof settings.sidebarCollapsed !== 'boolean') settings.sidebarCollapsed = false;
return settings;
}
async function saveSettings(settings) {
await setData(STORAGE_KEYS.settings, settings);
}
async function getCustomColors() {
const saved = await getData(STORAGE_KEYS.customColors, {});
return { ...defaultCustomColors, ...saved };
}
async function saveCustomColors(colors) {
await setData(STORAGE_KEYS.customColors, colors);
}
async function getExcludedDomains() {
return await getData(STORAGE_KEYS.excludedDomains, []);
}
async function saveExcludedDomains(domains) {
await setData(STORAGE_KEYS.excludedDomains, domains);
}
async function getBubbleHiddenState() {
return await getData(STORAGE_KEYS.bubbleHidden, false);
}
async function saveBubbleHiddenState(isHidden) {
await setData(STORAGE_KEYS.bubbleHidden, isHidden);
}
async function getButtonPosition() {
return await getData(STORAGE_KEYS.position, { vertical: 'bottom', horizontal: 'right' });
}
async function saveButtonPosition(position) {
await setData(STORAGE_KEYS.position, position);
}
async function getTheme() {
return await getData(STORAGE_KEYS.theme, 'default');
}
async function saveTheme(theme) {
await setData(STORAGE_KEYS.theme, theme);
}
async function getClickStats() {
return await getData(STORAGE_KEYS.clickStats, {});
}
async function incrementClickStat(url) {
const stats = await getClickStats();
stats[url] = (stats[url] || 0) + 1;
await setData(STORAGE_KEYS.clickStats, stats);
}
async function getFolders() {
return await getData(STORAGE_KEYS.folders, []);
}
async function saveFolders(folders) {
await setData(STORAGE_KEYS.folders, folders);
}
// --- Toast Notification ---
function showToast(message, type = 'success') {
const existing = shadowRoot.querySelector('.ulm-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.className = 'ulm-toast';
toast.dataset.type = type;
toast.textContent = message;
shadowRoot.appendChild(toast);
setTimeout(() => toast.classList.add('visible'), 10);
setTimeout(() => {
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
}, 2500);
}
// --- Favicon Helper ---
function getFaviconUrl(url) {
try {
const u = new URL(url);
return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=32`;
} catch { return ''; }
}
// --- Auto-Categorize ---
async function suggestCategory(url) {
const settings = await getSettings();
const domains = settings.autoCategorizeDomains || {};
try {
const hostname = new URL(url).hostname.replace('www.', '');
for (const [domain, category] of Object.entries(domains)) {
if (hostname.includes(domain)) return category;
}
} catch {}
return 'default';
}
// --- URL Normalization Helper ---
function normalizeUrl(url) {
try {
const urlObj = new URL(url);
let path = urlObj.pathname;
if (path.length > 1 && path.endsWith('/')) {
path = path.slice(0, -1);
}
return urlObj.hostname.replace(/^www\./, '') + path;
} catch {
return url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
}
}
// --- Duplicate Detection ---
async function checkDuplicate(url) {
const links = await getLinks();
const normalizedUrl = normalizeUrl(url);
return links.find(l => normalizeUrl(l.url) === normalizedUrl);
}
// --- Link Health Check ---
function checkLinkHealth(url) {
return new Promise((resolve) => {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'HEAD',
url: url,
timeout: 8000,
onload: (res) => resolve({ status: res.status, ok: res.status >= 200 && res.status < 400 }),
onerror: () => resolve({ status: 0, ok: false }),
ontimeout: () => resolve({ status: 0, ok: false })
});
} else {
resolve({ status: -1, ok: true });
}
});
}
// --- Helper: Time ago ---
function timeAgo(timestamp) {
if (!timestamp) return '';
const seconds = Math.floor((Date.now() - timestamp) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d ago`;
if (days < 30) return `${Math.floor(days / 7)}w ago`;
return `${Math.floor(days / 30)}mo ago`;
}
// --- Sort links ---
function sortLinks(links, mode) {
const sorted = [...links];
switch (mode) {
case 'az':
sorted.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }));
break;
case 'za':
sorted.sort((a, b) => b.label.localeCompare(a.label, undefined, { sensitivity: 'base' }));
break;
case 'recent':
sorted.sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
break;
case 'manual':
default:
break;
}
return sorted;
}
// --- Get available letters ---
function getAvailableLetters(links) {
const letters = new Set();
links.forEach(link => {
const firstChar = link.label.charAt(0).toUpperCase();
if (/[A-Z]/.test(firstChar)) {
letters.add(firstChar);
} else {
letters.add('#');
}
});
return letters;
}
// --- Highlight search text helper ---
function highlightText(text, query) {
if (!query) return text;
const words = query.toLowerCase().split(' ').filter(w => w);
let result = text;
words.forEach(word => {
const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
result = result.replace(regex, '<mark style="background:#0ff3;color:inherit;padding:0 1px;border-radius:2px;">$1</mark>');
});
return result;
}
// --- Theme Definitions ---
const themes = {
default: { name: 'Cyan Neon', colors: defaultCustomColors },
highContrast: { name: 'High Contrast', colors: { bubbleBackground: '#ffff00', bubbleText: '#000', bubbleGlow: '#ffff00', menuBackground: '#000', menuBorder: '#ffff00', linkBackground: '#000', linkText: '#ffff00', linkHover: '#ffff00', buttonBackground: '#333', buttonText: '#ffff00', buttonHover: '#ffff00' } },
ocean: { name: 'Ocean Blue', colors: { bubbleBackground: '#00bcd4', bubbleText: '#fff', bubbleGlow: '#00bcd4', menuBackground: '#0d2137', menuBorder: '#00bcd4', linkBackground: '#1a3a5c', linkText: '#e0f7fa', linkHover: '#00bcd4', buttonBackground: '#1a3a5c', buttonText: '#00bcd4', buttonHover: '#00bcd4' } },
sunset: { name: 'Sunset', colors: { bubbleBackground: '#ff6b6b', bubbleText: '#fff', bubbleGlow: '#ff6b6b', menuBackground: '#2d1b2e', menuBorder: '#ff6b6b', linkBackground: '#4a2c4a', linkText: '#ffeaa7', linkHover: '#ff6b6b', buttonBackground: '#4a2c4a', buttonText: '#ff6b6b', buttonHover: '#ff6b6b' } },
forest: { name: 'Forest', colors: { bubbleBackground: '#2ecc71', bubbleText: '#fff', bubbleGlow: '#2ecc71', menuBackground: '#1a2f23', menuBorder: '#2ecc71', linkBackground: '#2d4a3e', linkText: '#a8e6cf', linkHover: '#2ecc71', buttonBackground: '#2d4a3e', buttonText: '#2ecc71', buttonHover: '#2ecc71' } },
purple: { name: 'Purple Haze', colors: { bubbleBackground: '#a855f7', bubbleText: '#fff', bubbleGlow: '#a855f7', menuBackground: '#1e1033', menuBorder: '#a855f7', linkBackground: '#2d1f4a', linkText: '#e9d5ff', linkHover: '#a855f7', buttonBackground: '#2d1f4a', buttonText: '#a855f7', buttonHover: '#a855f7' } },
light: { name: 'Light Mode', colors: { bubbleBackground: '#3b82f6', bubbleText: '#fff', bubbleGlow: '#3b82f6', menuBackground: '#ffffff', menuBorder: '#3b82f6', linkBackground: '#f0f4f8', linkText: '#1e293b', linkHover: '#3b82f6', buttonBackground: '#e2e8f0', buttonText: '#3b82f6', buttonHover: '#3b82f6' } },
custom: { name: 'Custom', colors: null }
};
// --- Generate Dynamic Styles ---
function generateStyles(colors, settings) {
const bubbleSize = settings.bubbleSize || 60;
const animationEnabled = settings.animationsEnabled !== false;
return `
*, *::before, *::after {
all: revert;
box-sizing: border-box !important;
}
@keyframes ulm-pulse {
0% { transform: scale(1); box-shadow: 0 0 15px 3px ${colors.bubbleGlow}, 0 0 30px 10px ${colors.bubbleGlow}; }
50% { transform: scale(1.05); box-shadow: 0 0 20px 5px ${colors.bubbleGlow}, 0 0 40px 15px ${colors.bubbleGlow}; }
100% { transform: scale(1); box-shadow: 0 0 15px 3px ${colors.bubbleGlow}, 0 0 30px 10px ${colors.bubbleGlow}; }
}
@keyframes ulm-neonGlow {
0% { box-shadow: 0 0 10px ${colors.menuBorder}aa; }
50% { box-shadow: 0 0 15px ${colors.menuBorder}ee, 0 0 25px ${colors.menuBorder}99; }
100% { box-shadow: 0 0 10px ${colors.menuBorder}aa; }
}
@keyframes ulm-fadeIn {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes ulm-slideDown {
from { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
to { opacity: 1; max-height: 800px; }
}
:host {
all: initial !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif !important;
font-size: 14px !important;
line-height: 1.4 !important;
color: ${colors.linkText} !important;
}
.ulm-bubble {
position: fixed !important; width: ${bubbleSize}px !important; height: ${bubbleSize}px !important; min-width: ${bubbleSize}px !important; min-height: ${bubbleSize}px !important; max-width: ${bubbleSize}px !important; max-height: ${bubbleSize}px !important;
background-color: ${colors.bubbleBackground} !important;
border-radius: 50% !important;
box-shadow: 0 0 15px 3px ${colors.bubbleGlow}, 0 0 30px 10px ${colors.bubbleGlow} !important;
cursor: pointer !important; z-index: 2147483647 !important; display: flex !important; justify-content: center !important; align-items: center !important;
font-size: ${Math.floor(bubbleSize * 0.6)}px !important; font-weight: 900 !important; color: ${colors.bubbleText} !important; user-select: none !important;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important;
transition: transform 0.2s ease, box-shadow 0.2s ease !important;
${animationEnabled ? 'animation: ulm-pulse 3s infinite ease-in-out !important;' : ''}
border: none !important; outline: none !important; margin: 0 !important; padding: 0 !important; opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; transform: none !important; float: none !important; clear: none !important;
}
.ulm-bubble:hover { transform: scale(1.15) !important; box-shadow: 0 0 20px 5px ${colors.bubbleGlow}, 0 0 40px 15px ${colors.bubbleGlow} !important; }
.ulm-bubble.hidden { display: none !important; }
.ulm-show-button {
position: fixed !important; width: ${bubbleSize}px !important; height: ${bubbleSize}px !important; min-width: ${bubbleSize}px !important; min-height: ${bubbleSize}px !important;
cursor: pointer !important; z-index: 2147483646 !important; background-color: transparent !important;
border: 3px dashed ${colors.bubbleBackground}66 !important; border-radius: 50% !important; display: none !important;
margin: 0 !important; padding: 0 !important; opacity: 1 !important; visibility: visible !important;
}
.ulm-show-button:hover { border-color: ${colors.bubbleBackground} !important; }
.ulm-show-button.visible { display: block !important; }
.ulm-mini-menu {
position: fixed !important; z-index: 2147483647 !important; padding: 10px !important; border-radius: 8px !important;
display: none !important; flex-direction: column !important; gap: 8px !important;
background-color: ${colors.menuBackground} !important; border: 2px solid ${colors.menuBorder} !important;
${animationEnabled ? 'animation: ulm-neonGlow 4s infinite ease-in-out !important;' : ''}
box-shadow: 0 0 10px ${colors.menuBorder}aa !important;
width: auto !important; height: auto !important; min-width: 150px !important; opacity: 0 !important; visibility: hidden !important;
transition: opacity 0.2s ease, visibility 0.2s ease, transform 0.2s ease !important;
transform: scale(0.95) !important;
}
.ulm-mini-menu.visible { display: flex !important; opacity: 1 !important; visibility: visible !important; transform: scale(1) !important; }
.ulm-mini-menu button {
display: block !important; width: 100% !important; padding: 10px 15px !important; margin: 0 !important; color: ${colors.buttonText} !important;
border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.buttonBackground} !important; border-radius: 5px !important;
cursor: pointer !important; font-size: 14px !important; font-family: inherit !important; text-align: center !important;
transition: background-color 0.2s, color 0.2s !important; line-height: 1.4 !important; opacity: 1 !important; visibility: visible !important;
}
.ulm-mini-menu button:hover { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
.ulm-main-ui {
position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important;
width: 680px !important; max-width: 95vw !important; max-height: 90vh !important;
background-color: ${colors.menuBackground} !important; border: 2px solid ${colors.menuBorder} !important; border-radius: 12px !important;
padding: 0 !important; z-index: 2147483647 !important; display: none !important; flex-direction: column !important;
overflow: hidden !important;
${animationEnabled ? 'animation: ulm-fadeIn 0.3s ease, ulm-neonGlow 4s infinite ease-in-out !important;' : ''}
box-shadow: 0 0 20px ${colors.menuBorder}aa, 0 0 50px rgba(0,0,0,0.5) !important;
opacity: 1 !important; visibility: visible !important; margin: 0 !important; float: none !important;
transition: border-color 0.3s ease;
}
.ulm-main-ui.visible { display: flex !important; }
.ulm-main-ui.edit-mode { border-color: #f44336 !important; }
/* --- Layout: Header + Body(Sidebar + Content) --- */
.ulm-layout-header {
display: flex !important; justify-content: space-between !important; align-items: center !important;
border-bottom: 1px solid #555 !important; padding: 12px 15px !important; margin: 0 !important; flex-shrink: 0 !important;
}
.ulm-header-left { display: flex !important; flex-direction: column !important; gap: 2px !important; }
.ulm-layout-header h2 { margin: 0 !important; padding: 0 !important; color: ${colors.menuBorder} !important; text-shadow: 0 0 5px ${colors.menuBorder} !important; font-size: 18px !important; font-weight: bold !important; line-height: 1.2 !important; }
.ulm-header-info { font-size: 11px !important; color: ${colors.linkText}aa !important; margin: 0 !important; padding: 0 !important; }
.ulm-header-shortcuts { font-size: 10px !important; color: ${colors.linkText}66 !important; margin: 0 !important; }
.ulm-header-shortcuts kbd { background: ${colors.linkBackground} !important; border: 1px solid ${colors.menuBorder}44 !important; border-radius: 3px !important; padding: 1px 5px !important; font-size: 9px !important; font-family: monospace !important; }
.ulm-close-btn { background: none !important; border: none !important; color: ${colors.linkText} !important; font-size: 28px !important; cursor: pointer !important; padding: 0 !important; margin: 0 !important; width: 35px !important; height: 35px !important; min-width: 35px !important; min-height: 35px !important; border-radius: 50% !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; justify-content: center !important; line-height: 1 !important; }
.ulm-close-btn:hover { background-color: #f00 !important; color: #fff !important; transform: scale(1.1) !important; }
.ulm-layout-body {
display: flex !important; flex: 1 !important; min-height: 0 !important; overflow: hidden !important;
}
/* --- Sidebar --- */
.ulm-sidebar {
width: 170px !important; min-width: 170px !important; flex-shrink: 0 !important;
background-color: ${colors.linkBackground}88 !important;
border-right: 1px solid ${colors.menuBorder}33 !important;
display: flex !important; flex-direction: column !important;
overflow-y: auto !important; overflow-x: hidden !important;
transition: width 0.25s ease, min-width 0.25s ease, padding 0.25s ease, opacity 0.25s ease !important;
padding: 8px 0 !important;
}
.ulm-sidebar.collapsed {
width: 0px !important; min-width: 0px !important; padding: 0 !important; opacity: 0 !important; overflow: hidden !important;
border-right: none !important;
}
.ulm-sidebar-header {
display: flex !important; align-items: center !important; justify-content: space-between !important;
padding: 6px 12px 10px 12px !important;
border-bottom: 1px solid ${colors.menuBorder}22 !important;
margin-bottom: 4px !important; flex-shrink: 0 !important;
}
.ulm-sidebar-header span {
font-size: 11px !important; font-weight: 700 !important; text-transform: uppercase !important;
letter-spacing: 1px !important; color: ${colors.menuBorder} !important; white-space: nowrap !important;
}
.ulm-sidebar-toggle {
background: none !important; border: none !important; color: ${colors.linkText}88 !important;
cursor: pointer !important; font-size: 14px !important; padding: 2px 4px !important;
border-radius: 4px !important; transition: all 0.15s !important; line-height: 1 !important;
display: flex !important; align-items: center !important; justify-content: center !important;
width: 24px !important; height: 24px !important; min-width: 24px !important; min-height: 24px !important;
}
.ulm-sidebar-toggle:hover { background-color: ${colors.menuBorder}33 !important; color: ${colors.linkText} !important; }
.ulm-sidebar-expand-btn {
position: absolute !important; left: 0 !important; top: 50% !important; transform: translateY(-50%) !important;
width: 20px !important; height: 44px !important;
background-color: ${colors.linkBackground} !important;
border: 1px solid ${colors.menuBorder}44 !important;
border-left: none !important; border-radius: 0 6px 6px 0 !important;
cursor: pointer !important; color: ${colors.linkText}88 !important;
font-size: 10px !important; display: none !important;
align-items: center !important; justify-content: center !important;
z-index: 5 !important; transition: all 0.15s !important;
}
.ulm-sidebar-expand-btn:hover { background-color: ${colors.menuBorder}33 !important; color: ${colors.linkText} !important; }
.ulm-sidebar-expand-btn.visible { display: flex !important; }
.ulm-folder-nav { display: flex !important; flex-direction: column !important; gap: 1px !important; flex: 1 !important; }
.ulm-folder-nav-item {
display: flex !important; align-items: center !important; gap: 8px !important;
padding: 8px 12px !important; cursor: pointer !important;
color: ${colors.linkText}cc !important; font-size: 12px !important;
transition: all 0.15s ease !important; border: none !important;
background: transparent !important; width: 100% !important;
text-align: left !important; font-family: inherit !important;
white-space: nowrap !important; overflow: hidden !important;
position: relative !important; border-radius: 0 !important;
margin: 0 !important; min-height: 34px !important;
}
.ulm-folder-nav-item:hover { background-color: ${colors.menuBorder}18 !important; color: ${colors.linkText} !important; }
.ulm-folder-nav-item.active {
background-color: ${colors.menuBorder}22 !important; color: ${colors.menuBorder} !important;
border-left: 3px solid ${colors.menuBorder} !important;
font-weight: 600 !important;
}
.ulm-folder-nav-item .folder-icon { font-size: 14px !important; flex-shrink: 0 !important; width: 18px !important; text-align: center !important; }
.ulm-folder-nav-item .folder-name { flex: 1 !important; overflow: hidden !important; text-overflow: ellipsis !important; }
.ulm-folder-nav-item .folder-count {
font-size: 10px !important; background: ${colors.menuBorder}22 !important;
color: ${colors.linkText}88 !important; padding: 1px 6px !important;
border-radius: 10px !important; flex-shrink: 0 !important;
min-width: 20px !important; text-align: center !important;
}
.ulm-folder-nav-item .folder-delete-btn {
position: absolute !important; right: 6px !important; top: 50% !important; transform: translateY(-50%) !important;
width: 20px !important; height: 20px !important; min-width: 20px !important; min-height: 20px !important;
border-radius: 50% !important; border: none !important;
background: #a00 !important; color: #fff !important;
font-size: 12px !important; cursor: pointer !important;
display: none !important; align-items: center !important; justify-content: center !important;
transition: background 0.15s !important; line-height: 1 !important; padding: 0 !important;
}
.ulm-folder-nav-item .folder-delete-btn:hover { background: #f00 !important; }
.ulm-folder-nav-item.edit-mode .folder-delete-btn { display: flex !important; }
.ulm-folder-nav-item.edit-mode .folder-count { display: none !important; }
.ulm-sidebar-divider {
height: 1px !important; background: ${colors.menuBorder}22 !important;
margin: 6px 12px !important; flex-shrink: 0 !important;
}
/* --- Main Content Area --- */
.ulm-content {
flex: 1 !important; display: flex !important; flex-direction: column !important;
gap: 12px !important; padding: 15px !important; overflow-y: auto !important;
overflow-x: hidden !important; min-width: 0 !important;
position: relative !important;
}
.ulm-search-container { display: flex !important; gap: 8px !important; flex-shrink: 0 !important; }
.ulm-search-input { flex: 1 !important; padding: 10px 14px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.linkBackground} !important; color: ${colors.linkText} !important; border-radius: 6px !important; font-size: 14px !important; font-family: inherit !important; outline: none !important; margin: 0 !important; height: auto !important; min-height: 40px !important; transition: border-color 0.2s ease, box-shadow 0.2s ease !important; }
.ulm-search-input::placeholder { color: ${colors.linkText}88 !important; }
.ulm-search-input:focus { border-color: ${colors.buttonHover} !important; box-shadow: 0 0 5px ${colors.buttonHover}44 !important; }
.ulm-clear-search { padding: 10px 14px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.buttonBackground} !important; color: ${colors.buttonText} !important; border-radius: 6px !important; cursor: pointer !important; font-size: 14px !important; margin: 0 !important; min-width: 40px !important; min-height: 40px !important; transition: all 0.2s ease !important; }
.ulm-clear-search:hover { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
/* Sort and Filter Bar */
.ulm-sort-filter-bar { display: flex !important; align-items: center !important; gap: 8px !important; flex-shrink: 0 !important; flex-wrap: wrap !important; }
.ulm-category-filter { display: flex !important; align-items: center !important; gap: 6px !important; flex: 1 !important; min-width: 0 !important; }
.ulm-category-filter label { font-size: 12px !important; color: ${colors.linkText}cc !important; white-space: nowrap !important; }
.ulm-category-dropdown, .ulm-sort-dropdown { flex: 1 !important; padding: 7px 10px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.linkBackground} !important; color: ${colors.linkText} !important; border-radius: 6px !important; font-size: 13px !important; font-family: inherit !important; cursor: pointer !important; outline: none !important; min-width: 0 !important; transition: border-color 0.2s ease !important; }
.ulm-category-dropdown:focus, .ulm-sort-dropdown:focus { border-color: ${colors.buttonHover} !important; }
.ulm-category-dropdown option, .ulm-sort-dropdown option { background-color: ${colors.menuBackground} !important; color: ${colors.linkText} !important; }
.ulm-sort-dropdown { flex: none !important; }
.ulm-recent-btn { padding: 7px 12px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.buttonBackground} !important; color: ${colors.buttonText} !important; border-radius: 6px !important; cursor: pointer !important; font-size: 12px !important; font-family: inherit !important; white-space: nowrap !important; transition: all 0.2s !important; }
.ulm-recent-btn:hover { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
.ulm-recent-btn.active { background-color: ${colors.menuBorder} !important; color: ${colors.menuBackground} !important; }
/* Alphabetical Index Bar */
.ulm-alpha-bar { display: flex !important; flex-wrap: wrap !important; gap: 2px !important; padding: 6px 4px !important; background-color: ${colors.linkBackground}88 !important; border-radius: 6px !important; flex-shrink: 0 !important; justify-content: center !important; }
.ulm-alpha-bar.hidden { display: none !important; }
.ulm-alpha-letter {
width: 24px !important; height: 24px !important; min-width: 24px !important; min-height: 24px !important;
display: flex !important; align-items: center !important; justify-content: center !important;
font-size: 11px !important; font-weight: 600 !important; font-family: monospace !important;
border-radius: 4px !important; cursor: pointer !important; transition: all 0.15s !important;
border: none !important; padding: 0 !important; margin: 0 !important;
background-color: transparent !important; color: ${colors.linkText}99 !important;
}
.ulm-alpha-letter:hover { background-color: ${colors.menuBorder}44 !important; color: ${colors.linkText} !important; }
.ulm-alpha-letter.active { background-color: ${colors.menuBorder} !important; color: ${colors.menuBackground} !important; }
.ulm-alpha-letter.has-links { color: ${colors.linkText} !important; font-weight: 700 !important; }
.ulm-alpha-letter.disabled { color: ${colors.linkText}33 !important; cursor: default !important; pointer-events: none !important; }
.ulm-alpha-clear {
padding: 2px 8px !important; margin-left: 4px !important;
font-size: 10px !important; font-family: inherit !important;
border-radius: 4px !important; cursor: pointer !important;
border: 1px solid ${colors.menuBorder}66 !important;
background: ${colors.buttonBackground} !important; color: ${colors.buttonText} !important;
transition: all 0.15s !important; display: none !important; align-items: center !important;
}
.ulm-alpha-clear.visible { display: flex !important; }
.ulm-alpha-clear:hover { background: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
/* Recently Added Section */
.ulm-recent-section { display: none !important; flex-direction: column !important; gap: 6px !important; flex-shrink: 0 !important; overflow: hidden !important; transition: all 0.3s ease !important; }
.ulm-recent-section.visible { display: flex !important; }
.ulm-recent-header { display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 6px 0 !important; }
.ulm-recent-header h3 { margin: 0 !important; font-size: 14px !important; color: ${colors.menuBorder} !important; font-weight: 600 !important; }
.ulm-recent-header span { font-size: 11px !important; color: ${colors.linkText}88 !important; }
.ulm-recent-list { display: flex !important; flex-direction: column !important; gap: 4px !important; max-height: 180px !important; overflow-y: auto !important; padding: 4px !important; }
.ulm-recent-item { display: flex !important; align-items: center !important; gap: 8px !important; padding: 8px 12px !important; background-color: ${colors.linkBackground} !important; border: 1px solid ${colors.menuBorder}44 !important; border-radius: 6px !important; cursor: pointer !important; text-decoration: none !important; transition: all 0.2s !important; }
.ulm-recent-item:hover { background-color: ${colors.linkHover} !important; color: ${colors.menuBackground} !important; border-color: ${colors.menuBorder} !important; }
.ulm-recent-item .ulm-recent-icon { font-size: 16px !important; flex-shrink: 0 !important; opacity: 0.6 !important; }
.ulm-recent-item .ulm-recent-label { flex: 1 !important; font-size: 13px !important; color: ${colors.linkText} !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
.ulm-recent-item:hover .ulm-recent-label { color: ${colors.menuBackground} !important; }
.ulm-recent-item .ulm-recent-time { font-size: 10px !important; color: ${colors.linkText}66 !important; white-space: nowrap !important; flex-shrink: 0 !important; }
.ulm-recent-item:hover .ulm-recent-time { color: ${colors.menuBackground}aa !important; }
.ulm-recent-item .ulm-recent-cat-dot { width: 8px !important; height: 8px !important; border-radius: 50% !important; flex-shrink: 0 !important; }
/* Letter Group Headers */
.ulm-letter-group-header { display: flex !important; align-items: center !important; gap: 8px !important; padding: 4px 8px !important; margin-top: 4px !important; }
.ulm-letter-group-header:first-child { margin-top: 0 !important; }
.ulm-letter-group-char { font-size: 14px !important; font-weight: 800 !important; color: ${colors.menuBorder} !important; font-family: monospace !important; min-width: 20px !important; text-align: center !important; }
.ulm-letter-group-line { flex: 1 !important; height: 1px !important; background: ${colors.menuBorder}44 !important; }
.ulm-letter-group-count { font-size: 10px !important; color: ${colors.linkText}66 !important; }
.ulm-link-list { display: flex !important; flex-direction: column !important; gap: 6px !important; max-height: 250px !important; min-height: 60px !important; overflow-y: auto !important; overflow-x: hidden !important; padding: 5px !important; margin: 0 !important; flex-shrink: 0 !important; }
.ulm-link-wrapper { display: flex !important; align-items: center !important; gap: 8px !important; width: 100% !important; margin: 0 !important; padding: 0 !important; opacity: 1 !important; visibility: visible !important; min-height: 42px !important; border-radius: 6px; transition: opacity 0.2s ease !important; }
.ulm-link-wrapper.dragging { opacity: 0.5 !important; }
.ulm-link-wrapper.drag-over { border-top: 2px solid ${colors.menuBorder} !important; }
.ulm-link-wrapper.selected { background-color: ${colors.menuBorder}22; }
.ulm-drag-handle { cursor: grab !important; padding: 5px !important; color: ${colors.linkText}88 !important; font-size: 16px !important; user-select: none !important; flex-shrink: 0 !important; }
.ulm-drag-handle:active { cursor: grabbing !important; }
.ulm-link { flex: 1 !important; padding: 10px 14px !important; text-decoration: none !important; border-radius: 6px !important; font-size: 14px !important; font-family: inherit !important; color: ${colors.linkText} !important; background-color: ${colors.linkBackground} !important; border: 1px solid ${colors.menuBorder} !important; display: flex !important; justify-content: space-between !important; align-items: center !important; transition: background-color 0.2s ease, color 0.2s ease !important; cursor: pointer !important; margin: 0 !important; min-height: 42px !important; opacity: 1 !important; visibility: visible !important; }
.ulm-link:hover { background-color: ${colors.linkHover} !important; color: ${colors.menuBackground} !important; }
.ulm-link-content { display: flex; align-items: center; flex: 1; min-width: 0; }
.ulm-link-label { font-weight: 500 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 180px !important; }
.ulm-link-meta { display: flex !important; align-items: center !important; gap: 6px !important; flex-shrink: 0 !important; }
.ulm-shortcut-badge { font-size: 10px !important; padding: 3px 7px !important; background-color: ${colors.menuBorder}44 !important; border-radius: 4px !important; font-family: monospace !important; }
.ulm-click-count { font-size: 10px !important; color: ${colors.linkText}88 !important; }
.ulm-time-badge { font-size: 9px !important; color: ${colors.linkText}66 !important; font-style: italic !important; }
.ulm-category-badge { font-size: 10px !important; padding: 3px 7px !important; border-radius: 10px !important; border: 1px solid currentColor !important; }
.ulm-action-btn { width: 32px !important; height: 32px !important; min-width: 32px !important; min-height: 32px !important; border-radius: 50% !important; cursor: pointer !important; font-weight: bold !important; transition: background-color 0.2s ease !important; display: flex !important; justify-content: center !important; align-items: center !important; padding: 0 !important; margin: 0 !important; font-size: 16px !important; flex-shrink: 0 !important; border: none !important; }
.ulm-delete-btn { background-color: #a00 !important; border: 1px solid #f00 !important; color: #fff !important; }
.ulm-delete-btn:hover { background-color: #f00 !important; }
.ulm-edit-btn { background-color: ${colors.buttonBackground} !important; border: 1px solid ${colors.menuBorder} !important; color: ${colors.buttonText} !important; }
.ulm-edit-btn:hover { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
/* Accordion Sections */
.ulm-accordion { display: flex !important; flex-direction: column !important; gap: 0 !important; flex-shrink: 0 !important; border: 1px solid ${colors.menuBorder}44 !important; border-radius: 8px !important; overflow: hidden !important; }
.ulm-accordion-header {
display: flex !important; align-items: center !important; justify-content: space-between !important;
padding: 12px 14px !important; cursor: pointer !important;
background-color: ${colors.linkBackground} !important; color: ${colors.linkText} !important;
border-bottom: 1px solid ${colors.menuBorder}22 !important;
font-size: 13px !important; font-weight: 600 !important; font-family: inherit !important;
transition: all 0.2s ease !important; user-select: none !important;
margin: 0 !important; border: none !important; width: 100% !important; text-align: left !important;
}
.ulm-accordion-header:hover { background-color: ${colors.menuBorder}22 !important; }
.ulm-accordion-header.active { background-color: ${colors.menuBorder}18 !important; color: ${colors.menuBorder} !important; }
.ulm-accordion-arrow { transition: transform 0.3s ease !important; font-size: 12px !important; opacity: 0.6 !important; }
.ulm-accordion-header.active .ulm-accordion-arrow { transform: rotate(180deg) !important; }
.ulm-accordion-body {
max-height: 0 !important; overflow: hidden !important;
transition: max-height 0.35s ease, padding 0.35s ease, opacity 0.25s ease !important;
opacity: 0 !important; padding: 0 14px !important;
background-color: ${colors.menuBackground} !important;
}
.ulm-accordion-body.open {
max-height: 800px !important; opacity: 1 !important;
padding: 14px !important;
}
.ulm-form-group { display: flex !important; flex-direction: column !important; gap: 6px !important; margin: 0 !important; }
.ulm-form-group label { font-size: 12px !important; color: ${colors.linkText}cc !important; margin: 0 !important; padding: 0 !important; }
.ulm-form-group input, .ulm-form-group select, .ulm-form-group textarea { padding: 10px 12px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.linkBackground} !important; color: ${colors.linkText} !important; border-radius: 6px !important; font-size: 14px !important; font-family: inherit !important; margin: 0 !important; outline: none !important; min-height: 40px !important; transition: border-color 0.2s ease !important; }
.ulm-form-group input:focus, .ulm-form-group select:focus, .ulm-form-group textarea:focus { border-color: ${colors.buttonHover} !important; }
.ulm-form-row { display: flex !important; gap: 10px !important; margin: 0 !important; }
.ulm-form-row .ulm-form-group { flex: 1 !important; }
.ulm-btn { padding: 10px 18px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.buttonBackground} !important; color: ${colors.buttonText} !important; border-radius: 6px !important; cursor: pointer !important; font-size: 13px !important; font-family: inherit !important; transition: all 0.2s !important; margin: 0 !important; text-align: center !important; min-height: 40px !important; }
.ulm-btn:hover { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
.ulm-btn.active { background-color: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
.ulm-btn-primary { background-color: ${colors.menuBorder} !important; color: ${colors.menuBackground} !important; }
.ulm-btn-primary:hover { background-color: ${colors.buttonHover} !important; filter: brightness(1.1) !important; }
.ulm-btn-danger { background-color: #a00 !important; border-color: #f00 !important; color: #fff !important; }
.ulm-btn-danger:hover { background-color: #f00 !important; }
.ulm-btn-group { display: flex !important; gap: 8px !important; flex-wrap: wrap !important; margin: 0 !important; }
.ulm-btn-group .ulm-btn { flex: 1 !important; min-width: 100px !important; }
.ulm-color-grid { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 10px !important; margin: 0 !important; }
.ulm-color-item { display: flex !important; flex-direction: column !important; gap: 4px !important; }
.ulm-color-item label { font-size: 11px !important; color: ${colors.linkText}aa !important; }
.ulm-color-item input[type="color"] { width: 100% !important; height: 40px !important; padding: 3px !important; border: 1px solid ${colors.menuBorder} !important; border-radius: 6px !important; cursor: pointer !important; background-color: ${colors.linkBackground} !important; }
/* Settings - Collapsible Groups */
.ulm-settings-section { margin-bottom: 12px !important; }
.ulm-settings-section:last-child { margin-bottom: 0 !important; }
.ulm-settings-section-header {
display: flex !important; align-items: center !important; justify-content: space-between !important;
padding: 8px 10px !important; cursor: pointer !important;
background-color: ${colors.linkBackground}88 !important; border-radius: 6px !important;
font-size: 12px !important; font-weight: 600 !important; color: ${colors.menuBorder} !important;
transition: all 0.2s ease !important; user-select: none !important;
}
.ulm-settings-section-header:hover { background-color: ${colors.linkBackground} !important; }
.ulm-settings-section-header .ulm-ss-arrow { transition: transform 0.3s ease !important; font-size: 10px !important; }
.ulm-settings-section-header.open .ulm-ss-arrow { transform: rotate(180deg) !important; }
.ulm-settings-section-body {
max-height: 0 !important; overflow: hidden !important;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.2s ease !important;
opacity: 0 !important; padding: 0 4px !important;
}
.ulm-settings-section-body.open {
max-height: 600px !important; opacity: 1 !important; padding: 10px 4px !important;
}
.ulm-settings-grid { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 10px !important; margin: 0 !important; }
.ulm-setting-item { display: flex !important; align-items: center !important; justify-content: space-between !important; padding: 10px !important; background-color: ${colors.linkBackground} !important; border-radius: 6px !important; min-height: 45px !important; }
.ulm-setting-item label { font-size: 12px !important; color: ${colors.linkText} !important; }
.ulm-toggle-switch { position: relative !important; width: 44px !important; height: 24px !important; flex-shrink: 0 !important; }
.ulm-toggle-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; position: absolute !important; }
.ulm-toggle-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #555 !important; transition: 0.3s !important; border-radius: 24px !important; }
.ulm-toggle-slider:before { position: absolute !important; content: "" !important; height: 18px !important; width: 18px !important; left: 3px !important; bottom: 3px !important; background-color: white !important; transition: 0.3s !important; border-radius: 50% !important; }
.ulm-toggle-switch input:checked + .ulm-toggle-slider { background-color: ${colors.menuBorder} !important; }
.ulm-toggle-switch input:checked + .ulm-toggle-slider:before { transform: translateX(20px) !important; }
.ulm-range-container { display: flex !important; align-items: center !important; gap: 12px !important; }
.ulm-range-container input[type="range"] { flex: 1 !important; height: 8px !important; -webkit-appearance: none !important; appearance: none !important; background: ${colors.linkBackground} !important; border-radius: 4px !important; outline: none !important; border: none !important; padding: 0 !important; margin: 0 !important; }
.ulm-range-container input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none !important; appearance: none !important; width: 20px !important; height: 20px !important; background: ${colors.menuBorder} !important; border-radius: 50% !important; cursor: pointer !important; border: none !important; }
.ulm-range-value { min-width: 45px !important; text-align: center !important; color: ${colors.linkText} !important; font-size: 13px !important; }
.ulm-theme-grid { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 8px !important; margin: 0 !important; }
.ulm-theme-option { padding: 10px 8px !important; border: 2px solid transparent !important; border-radius: 8px !important; cursor: pointer !important; text-align: center !important; font-size: 11px !important; transition: all 0.2s !important; background-color: ${colors.linkBackground} !important; color: ${colors.linkText} !important; }
.ulm-theme-option:hover { border-color: ${colors.menuBorder}88 !important; }
.ulm-theme-option.active { border-color: ${colors.menuBorder} !important; }
.ulm-theme-preview { width: 100% !important; height: 35px !important; border-radius: 5px !important; margin-bottom: 6px !important; }
.ulm-position-grid { display: grid !important; grid-template-columns: repeat(2, 1fr) !important; gap: 8px !important; margin: 0 !important; }
.ulm-backup-area { width: 100% !important; height: 120px !important; resize: vertical !important; font-family: monospace !important; font-size: 12px !important; }
/* Toast Notifications */
.ulm-toast {
position: fixed !important; bottom: 30px !important; left: 50% !important; transform: translateX(-50%) translateY(20px) !important;
padding: 12px 24px !important; border-radius: 8px !important; font-size: 14px !important; font-family: inherit !important;
z-index: 2147483647 !important; opacity: 0 !important; transition: all 0.3s ease !important; pointer-events: none !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important; white-space: nowrap !important;
background-color: ${colors.menuBorder} !important; color: ${colors.menuBackground} !important;
}
.ulm-toast[data-type="error"] { background-color: #f44336 !important; color: #fff !important; }
.ulm-toast[data-type="warning"] { background-color: #ff9800 !important; color: #000 !important; }
.ulm-toast[data-type="info"] { background-color: #2196f3 !important; color: #fff !important; }
.ulm-toast.visible { opacity: 1 !important; transform: translateX(-50%) translateY(0) !important; }
/* Favicon */
.ulm-favicon { width: 16px !important; height: 16px !important; min-width: 16px !important; min-height: 16px !important; border-radius: 2px !important; flex-shrink: 0 !important; margin-right: 6px !important; vertical-align: middle !important; }
/* Tags */
.ulm-tag { display: inline-block !important; font-size: 9px !important; padding: 2px 6px !important; border-radius: 8px !important; background-color: ${colors.menuBorder}33 !important; color: ${colors.menuBorder} !important; margin-left: 3px !important; white-space: nowrap !important; }
.ulm-tags-container { display: flex !important; flex-wrap: wrap !important; gap: 3px !important; align-items: center !important; }
.ulm-tag-input-wrapper { display: flex !important; flex-wrap: wrap !important; gap: 4px !important; padding: 6px !important; border: 1px solid ${colors.menuBorder} !important; background-color: ${colors.linkBackground} !important; border-radius: 6px !important; min-height: 40px !important; align-items: center !important; }
.ulm-tag-input-wrapper .ulm-tag { cursor: pointer !important; }
.ulm-tag-input-wrapper .ulm-tag:hover { background-color: #f44336 !important; color: #fff !important; }
.ulm-tag-input-wrapper input { border: none !important; background: transparent !important; color: ${colors.linkText} !important; font-size: 13px !important; outline: none !important; flex: 1 !important; min-width: 80px !important; padding: 4px !important; }
/* Health Check */
.ulm-health-dot { width: 8px !important; height: 8px !important; min-width: 8px !important; border-radius: 50% !important; flex-shrink: 0 !important; margin-right: 6px !important; }
.ulm-health-dot.ok { background-color: #4caf50 !important; }
.ulm-health-dot.error { background-color: #f44336 !important; }
.ulm-health-dot.checking { background-color: #ff9800 !important; animation: ulm-pulse 1.5s infinite !important; }
.ulm-health-dot.unknown { background-color: #888 !important; }
/* Empty State */
.ulm-empty-state {
display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important;
padding: 30px 20px !important; text-align: center !important; gap: 10px !important;
}
.ulm-empty-state-icon { font-size: 40px !important; opacity: 0.3 !important; }
.ulm-empty-state-title { font-size: 15px !important; font-weight: 600 !important; color: ${colors.linkText}aa !important; margin: 0 !important; }
.ulm-empty-state-subtitle { font-size: 12px !important; color: ${colors.linkText}66 !important; margin: 0 !important; }
.ulm-no-results { text-align: center !important; color: ${colors.linkText}88 !important; padding: 25px !important; font-size: 14px !important; }
.ulm-exclude-list { display: flex !important; flex-direction: column !important; gap: 6px !important; max-height: 120px !important; overflow-y: auto !important; margin: 0 !important; padding: 5px !important; }
.ulm-exclude-wrapper { display: flex !important; align-items: center !important; gap: 8px !important; }
.ulm-exclude-wrapper span { flex: 1 !important; padding: 8px 12px !important; background-color: ${colors.linkBackground} !important; border: 1px solid ${colors.menuBorder} !important; border-radius: 6px !important; font-size: 13px !important; color: ${colors.linkText} !important; }
.ulm-modal-overlay { position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; width: 100vw !important; height: 100vh !important; background: rgba(0,0,0,0.75) !important; z-index: 2147483647 !important; display: flex !important; justify-content: center !important; align-items: center !important; }
.ulm-modal-content { background: ${colors.menuBackground} !important; border: 2px solid ${colors.menuBorder} !important; border-radius: 12px !important; padding: 20px !important; max-width: 400px !important; width: 90% !important; }
.ulm-modal-content h3 { color: ${colors.menuBorder} !important; margin: 0 0 15px 0 !important; font-size: 16px !important; }
/* Context Menu */
.ulm-context-menu { position:fixed; background: ${colors.menuBackground}; border:1px solid ${colors.menuBorder}; border-radius:8px; padding:5px 0; z-index:999999; display:none; min-width:160px; box-shadow:0 4px 15px rgba(0,0,0,0.4); }
.ulm-ctx-item { padding:8px 16px; cursor:pointer; color:${colors.linkText}; font-size:13px; transition: background 0.15s ease !important; }
.ulm-ctx-item:hover { background: ${colors.linkHover}; color: ${colors.menuBackground}; }
/* Generic Manager Item */
.ulm-manager-list { display: flex !important; flex-direction: column !important; gap: 8px !important; max-height: 200px !important; overflow-y: auto !important; padding: 5px !important; margin: 0 !important; }
.ulm-manager-item { display: flex !important; align-items: center !important; gap: 8px !important; padding: 8px 10px !important; background-color: ${colors.linkBackground} !important; border: 1px solid ${colors.menuBorder}66 !important; border-radius: 6px !important; }
.ulm-manager-item .item-color { width: 32px !important; height: 32px !important; border: none !important; border-radius: 4px !important; cursor: pointer !important; padding: 0 !important; flex-shrink: 0 !important; }
.ulm-manager-item .item-name { flex: 1 !important; font-size: 13px !important; color: ${colors.linkText} !important; padding: 5px 8px !important; background: transparent !important; border: 1px solid transparent !important; border-radius: 4px !important; }
.ulm-manager-item .item-name:focus, .ulm-manager-item .item-name.editing { border-color: ${colors.menuBorder} !important; background: ${colors.menuBackground} !important; outline: none !important; }
.ulm-manager-item .item-count { font-size: 11px !important; color: ${colors.linkText}88 !important; padding: 2px 6px !important; background: ${colors.menuBorder}22 !important; border-radius: 10px !important; }
.ulm-manager-item .item-actions { display: flex !important; gap: 4px !important; }
.ulm-manager-item .item-btn { width: 28px !important; height: 28px !important; min-width: 28px !important; min-height: 28px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 14px !important; display: flex !important; align-items: center !important; justify-content: center !important; padding: 0 !important; margin: 0 !important; border: 1px solid ${colors.menuBorder}66 !important; background: ${colors.buttonBackground} !important; color: ${colors.buttonText} !important; transition: all 0.2s !important; }
.ulm-manager-item .item-btn:hover { background: ${colors.buttonHover} !important; color: ${colors.menuBackground} !important; }
.ulm-manager-item .item-btn.delete:hover { background: #f00 !important; border-color: #f00 !important; color: #fff !important; }
/* Default Sections Checkboxes */
.ulm-defaults-grid { display: flex !important; flex-direction: column !important; gap: 6px !important; }
.ulm-default-section-item {
display: flex !important; align-items: center !important; gap: 8px !important;
padding: 6px 8px !important; background: ${colors.linkBackground} !important;
border-radius: 4px !important; cursor: pointer !important;
}
.ulm-default-section-item:hover { background: ${colors.menuBorder}18 !important; }
.ulm-default-section-item input[type="checkbox"] {
width: 16px !important; height: 16px !important; cursor: pointer !important;
accent-color: ${colors.menuBorder} !important; flex-shrink: 0 !important;
margin: 0 !important; padding: 0 !important;
}
.ulm-default-section-item span { font-size: 12px !important; color: ${colors.linkText} !important; }
/* Scrollbar Styles */
.ulm-link-list::-webkit-scrollbar, .ulm-exclude-list::-webkit-scrollbar, .ulm-content::-webkit-scrollbar, .ulm-manager-list::-webkit-scrollbar, .ulm-recent-list::-webkit-scrollbar, .ulm-sidebar::-webkit-scrollbar { width: 8px !important; }
.ulm-link-list::-webkit-scrollbar-track, .ulm-exclude-list::-webkit-scrollbar-track, .ulm-content::-webkit-scrollbar-track, .ulm-manager-list::-webkit-scrollbar-track, .ulm-recent-list::-webkit-scrollbar-track, .ulm-sidebar::-webkit-scrollbar-track { background: ${colors.linkBackground} !important; border-radius: 4px !important; }
.ulm-link-list::-webkit-scrollbar-thumb, .ulm-exclude-list::-webkit-scrollbar-thumb, .ulm-content::-webkit-scrollbar-thumb, .ulm-manager-list::-webkit-scrollbar-thumb, .ulm-recent-list::-webkit-scrollbar-thumb, .ulm-sidebar::-webkit-scrollbar-thumb { background: ${colors.menuBorder}88 !important; border-radius: 4px !important; }
.ulm-link-list::-webkit-scrollbar-thumb:hover, .ulm-exclude-list::-webkit-scrollbar-thumb:hover, .ulm-content::-webkit-scrollbar-thumb:hover, .ulm-manager-list::-webkit-scrollbar-thumb:hover, .ulm-recent-list::-webkit-scrollbar-thumb:hover, .ulm-sidebar::-webkit-scrollbar-thumb:hover { background: ${colors.menuBorder} !important; }
`;
}
// --- Create Shadow DOM Container ---
function createShadowContainer() {
const container = document.createElement('div');
container.id = 'universal-link-manager-container';
container.style.cssText = 'all: initial !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 0 !important; height: 0 !important; z-index: 2147483647 !important; pointer-events: none !important;';
document.body.appendChild(container);
const shadow = container.attachShadow({ mode: 'closed' });
return shadow;
}
// --- UI Elements ---
let shadowRoot = null;
let styleElement = null;
let bubble = null;
let mainUI = null;
let showBubbleButton = null;
let bubbleMenu = null;
// --- Apply Theme ---
async function applyTheme(themeName) {
const settings = await getSettings();
let colors;
if (themeName === 'custom') {
colors = await getCustomColors();
} else if (themes[themeName]) {
colors = themes[themeName].colors;
} else {
colors = defaultCustomColors;
}
if (styleElement) {
styleElement.textContent = generateStyles(colors, settings);
}
if (bubble) {
bubble.textContent = settings.bubbleIcon;
bubble.style.width = settings.bubbleSize + 'px';
bubble.style.height = settings.bubbleSize + 'px';
bubble.style.fontSize = Math.floor(settings.bubbleSize * 0.6) + 'px';
}
}
// --- Populate Category Dropdown ---
async function populateCategoryDropdown(links, categories) {
const dropdown = shadowRoot.querySelector('.ulm-category-dropdown');
if (!dropdown) return;
dropdown.innerHTML = '';
const allOption = document.createElement('option');
allOption.value = 'all';
allOption.textContent = `All Categories (${links.length})`;
dropdown.appendChild(allOption);
categories.forEach(cat => {
const count = links.filter(l => l.category === cat.name).length;
const option = document.createElement('option');
option.value = cat.name;
option.textContent = `${cat.name.charAt(0).toUpperCase() + cat.name.slice(1)} (${count})`;
if (cat.name === currentCategory) option.selected = true;
dropdown.appendChild(option);
});
if (currentCategory !== 'all') {
dropdown.value = currentCategory;
}
}
// --- Build Alphabetical Index Bar ---
async function buildAlphaBar(links) {
const alphaBar = shadowRoot.querySelector('.ulm-alpha-bar');
if (!alphaBar) return;
let filteredLinks = links;
if (currentCategory !== 'all') {
filteredLinks = filteredLinks.filter(link => link.category === currentCategory);
}
const availableLetters = getAvailableLetters(filteredLinks);
const allLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('');
alphaBar.innerHTML = '';
const shouldShow = currentSortMode === 'az' || currentSortMode === 'za';
alphaBar.classList.toggle('hidden', !shouldShow);
if (!shouldShow) return;
allLetters.forEach(letter => {
const btn = document.createElement('button');
btn.className = 'ulm-alpha-letter';
btn.textContent = letter;
const hasLinks = availableLetters.has(letter);
btn.classList.toggle('has-links', hasLinks);
btn.classList.toggle('disabled', !hasLinks);
btn.classList.toggle('active', activeLetter === letter);
btn.addEventListener('click', async () => {
if (!hasLinks) return;
activeLetter = (activeLetter === letter) ? null : letter;
await refreshUI();
});
alphaBar.appendChild(btn);
});
const clearBtn = document.createElement('button');
clearBtn.className = 'ulm-alpha-clear';
clearBtn.textContent = '✕ Clear';
clearBtn.classList.toggle('visible', !!activeLetter);
clearBtn.addEventListener('click', async () => {
activeLetter = null;
await refreshUI();
});
alphaBar.appendChild(clearBtn);
}
// --- Populate Recently Added Section ---
async function populateRecentSection(links, settings, categories) {
const recentSection = shadowRoot.querySelector('.ulm-recent-section');
if (!recentSection) return;
recentSection.classList.toggle('visible', showRecentlyAdded);
if (!showRecentlyAdded) return;
const recentList = recentSection.querySelector('.ulm-recent-list');
const recentCountEl = recentSection.querySelector('.ulm-recent-count');
const recentLinks = [...links]
.filter(l => l.addedAt)
.sort((a, b) => b.addedAt - a.addedAt)
.slice(0, settings.recentCount || 10);
if (recentCountEl) {
recentCountEl.textContent = `Showing ${recentLinks.length} most recent`;
}
recentList.innerHTML = '';
if (recentLinks.length === 0) {
recentList.innerHTML = '<div class="ulm-empty-state"><div class="ulm-empty-state-icon">🕐</div><div class="ulm-empty-state-title">No recent links</div><div class="ulm-empty-state-subtitle">Links you add will show up here</div></div>';
return;
}
recentLinks.forEach(linkData => {
const item = document.createElement('a');
item.className = 'ulm-recent-item';
item.href = linkData.url;
item.target = settings.openInNewTab ? '_blank' : '_self';
const cat = categories.find(c => c.name === linkData.category);
item.innerHTML = `
<span class="ulm-recent-cat-dot" style="background-color: ${cat?.color || '#888'}"></span>
<span class="ulm-recent-icon">🕐</span>
<span class="ulm-recent-label">${linkData.label}</span>
<span class="ulm-recent-time">${timeAgo(linkData.addedAt)}</span>
`;
item.addEventListener('click', () => incrementClickStat(linkData.url));
recentList.appendChild(item);
});
}
// --- Populate Link List ---
async function populateLinkList(allLinks, settings, categories, clickStats) {
const linkListElement = shadowRoot.querySelector('.ulm-link-list');
linkListElement.innerHTML = '';
let filteredLinks = allLinks;
if (currentCategory !== 'all') {
filteredLinks = filteredLinks.filter(link => link.category === currentCategory);
}
if (currentFolder) {
filteredLinks = filteredLinks.filter(link => link.folder === currentFolder);
}
if (searchQuery) {
const searchWords = searchQuery.toLowerCase().split(' ').filter(w => w);
filteredLinks = filteredLinks.filter(link => {
const searchableText = [link.label, link.url, ...(link.tags || []), link.folder || ''].join(' ').toLowerCase();
return searchWords.every(word => searchableText.includes(word));
});
}
if (activeLetter) {
filteredLinks = filteredLinks.filter(link => {
const firstChar = link.label.charAt(0).toUpperCase();
return activeLetter === '#' ? !/[A-Z]/.test(firstChar) : firstChar === activeLetter;
});
}
filteredLinks = sortLinks(filteredLinks, currentSortMode);
if (filteredLinks.length === 0) {
if (searchQuery) {
linkListElement.innerHTML = '<div class="ulm-empty-state"><div class="ulm-empty-state-icon">🔍</div><div class="ulm-empty-state-title">No matches found</div><div class="ulm-empty-state-subtitle">Try a different search term</div></div>';
} else if (allLinks.length === 0) {
linkListElement.innerHTML = '<div class="ulm-empty-state"><div class="ulm-empty-state-icon">🔗</div><div class="ulm-empty-state-title">No links yet</div><div class="ulm-empty-state-subtitle">Add your first link below or press <kbd style="background:#444;padding:2px 6px;border-radius:3px;font-size:11px;">Ctrl+Alt+A</kbd> to save this page</div></div>';
} else {
linkListElement.innerHTML = '<div class="ulm-empty-state"><div class="ulm-empty-state-icon">📂</div><div class="ulm-empty-state-title">No links in this view</div><div class="ulm-empty-state-subtitle">Try changing filters or category</div></div>';
}
return;
}
const showGroups = (currentSortMode === 'az' || currentSortMode === 'za') && !activeLetter && !searchQuery;
let lastLetter = null;
filteredLinks.forEach((linkData) => {
const originalIndex = allLinks.indexOf(linkData);
if (showGroups) {
const firstChar = linkData.label.charAt(0).toUpperCase();
const groupLetter = /[A-Z]/.test(firstChar) ? firstChar : '#';
if (groupLetter !== lastLetter) {
lastLetter = groupLetter;
const groupCount = filteredLinks.filter(l => (l.label.charAt(0).toUpperCase() === groupLetter) || (groupLetter === '#' && !/[A-Z]/.test(l.label.charAt(0).toUpperCase()))).length;
const header = document.createElement('div');
header.className = 'ulm-letter-group-header';
header.innerHTML = `<span class="ulm-letter-group-char">${groupLetter}</span><div class="ulm-letter-group-line"></div><span class="ulm-letter-group-count">${groupCount}</span>`;
linkListElement.appendChild(header);
}
}
const linkWrapper = document.createElement('div');
linkWrapper.className = 'ulm-link-wrapper';
linkWrapper.draggable = currentSortMode === 'manual';
linkWrapper.dataset.index = originalIndex;
if (currentSortMode === 'manual') {
linkWrapper.innerHTML = '<span class="ulm-drag-handle">⋮⋮</span>';
}
const link = document.createElement('a');
link.href = linkData.url;
link.target = settings.openInNewTab ? '_blank' : '_self';
link.className = 'ulm-link';
const linkContent = document.createElement('div');
linkContent.className = 'ulm-link-content';
const healthDot = document.createElement('div');
healthDot.className = `ulm-health-dot ${linkData.health.status || 'unknown'}`;
linkContent.appendChild(healthDot);
if (settings.showFavicons) {
const favicon = document.createElement('img');
favicon.className = 'ulm-favicon';
favicon.src = getFaviconUrl(linkData.url);
favicon.onerror = () => { favicon.style.display = 'none'; };
linkContent.appendChild(favicon);
}
const labelSpan = document.createElement('span');
labelSpan.className = 'ulm-link-label';
if (searchQuery) {
labelSpan.innerHTML = highlightText(linkData.label, searchQuery);
} else {
labelSpan.textContent = linkData.label;
}
linkContent.appendChild(labelSpan);
if (linkData.tags && linkData.tags.length > 0) {
const tagsToDisplay = linkData.tags.slice(0, 2);
tagsToDisplay.forEach(tag => {
const tagEl = document.createElement('span');
tagEl.className = 'ulm-tag';
tagEl.textContent = tag;
linkContent.appendChild(tagEl);
});
if (linkData.tags.length > 2) {
const moreTag = document.createElement('span');
moreTag.className = 'ulm-tag';
moreTag.textContent = `+${linkData.tags.length - 2}`;
linkContent.appendChild(moreTag);
}
}
link.appendChild(linkContent);
const metaSpan = document.createElement('span');
metaSpan.className = 'ulm-link-meta';
if (linkData.shortcut) {
metaSpan.innerHTML += `<span class="ulm-shortcut-badge">${linkData.shortcut}</span>`;
}
if (settings.showClickCount && clickStats[linkData.url]) {
metaSpan.innerHTML += `<span class="ulm-click-count">(${clickStats[linkData.url]})</span>`;
}
if (currentSortMode === 'recent' && linkData.addedAt) {
metaSpan.innerHTML += `<span class="ulm-time-badge">${timeAgo(linkData.addedAt)}</span>`;
}
if (currentCategory === 'all' && linkData.category && linkData.category !== 'default') {
const cat = categories.find(c => c.name === linkData.category);
const catBadge = document.createElement('span');
catBadge.className = 'ulm-category-badge';
catBadge.textContent = linkData.category;
if(cat) catBadge.style.color = cat.color;
metaSpan.appendChild(catBadge);
}
link.appendChild(metaSpan);
link.addEventListener('click', (e) => {
if (!isDeleteMode) incrementClickStat(linkData.url);
});
linkWrapper.appendChild(link);
if (isDeleteMode) {
const editButton = document.createElement('button');
editButton.className = 'ulm-action-btn ulm-edit-btn';
editButton.innerHTML = '✎';
editButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showEditLinkModal(originalIndex); };
linkWrapper.appendChild(editButton);
const deleteButton = document.createElement('button');
deleteButton.className = 'ulm-action-btn ulm-delete-btn';
deleteButton.innerHTML = '×';
deleteButton.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
const currentSettings = await getSettings();
if (!currentSettings.confirmDelete || confirm(`Delete "${linkData.label}"?`)) {
const allLinksMutable = await getLinks();
allLinksMutable.splice(originalIndex, 1);
await saveLinks(allLinksMutable);
await refreshUI();
}
};
linkWrapper.appendChild(deleteButton);
}
if (currentSortMode === 'manual') {
linkWrapper.addEventListener('dragstart', () => { draggedItem = linkWrapper; linkWrapper.classList.add('dragging'); });
linkWrapper.addEventListener('dragend', () => {
linkWrapper.classList.remove('dragging');
shadowRoot.querySelectorAll('.ulm-link-wrapper.drag-over').forEach(item => item.classList.remove('drag-over'));
});
linkWrapper.addEventListener('dragover', (e) => { e.preventDefault(); if (draggedItem !== linkWrapper) linkWrapper.classList.add('drag-over'); });
linkWrapper.addEventListener('dragleave', () => linkWrapper.classList.remove('drag-over'));
linkWrapper.addEventListener('drop', async (e) => {
e.preventDefault();
linkWrapper.classList.remove('drag-over');
if (draggedItem && draggedItem !== linkWrapper) {
const fromIndex = parseInt(draggedItem.dataset.index);
const toIndex = parseInt(linkWrapper.dataset.index);
const allLinksMutable = await getLinks();
const [movedItem] = allLinksMutable.splice(fromIndex, 1);
allLinksMutable.splice(toIndex, 0, movedItem);
await saveLinks(allLinksMutable);
await refreshUI();
}
});
}
linkListElement.appendChild(linkWrapper);
});
}
// --- Edit Link Modal ---
async function showEditLinkModal(index) {
const links = await getLinks();
const categories = await getCategories();
const linkData = links[index];
const modal = document.createElement('div');
modal.className = 'ulm-modal-overlay';
modal.innerHTML = `
<div class="ulm-modal-content">
<h3>Edit Link</h3>
<div class="ulm-form-group"><label>Label</label><input type="text" class="ulm-edit-label" value="${linkData.label}"></div>
<div class="ulm-form-group"><label>URL</label><input type="text" class="ulm-edit-url" value="${linkData.url}"></div>
<div class="ulm-form-group"><label>Category</label><select class="ulm-edit-category"></select></div>
<div class="ulm-form-group"><label>Tags (comma separated)</label><input type="text" class="ulm-edit-tags" value="${(linkData.tags || []).join(', ')}"></div>
<div class="ulm-form-group"><label>Folder</label><input type="text" class="ulm-edit-folder" value="${linkData.folder || ''}"></div>
<div class="ulm-btn-group" style="margin-top: 15px;"><button class="ulm-btn ulm-btn-primary ulm-save-edit">Save</button><button class="ulm-btn ulm-cancel-edit">Cancel</button></div>
</div>
`;
shadowRoot.appendChild(modal);
const select = modal.querySelector('.ulm-edit-category');
categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.name;
option.textContent = cat.name.charAt(0).toUpperCase() + cat.name.slice(1);
option.selected = (cat.name === linkData.category);
select.appendChild(option);
});
modal.querySelector('.ulm-save-edit').onclick = async () => {
const newLabel = modal.querySelector('.ulm-edit-label').value.trim();
const newUrl = modal.querySelector('.ulm-edit-url').value.trim();
if (newLabel && newUrl) {
links[index].label = newLabel;
links[index].url = newUrl;
links[index].category = modal.querySelector('.ulm-edit-category').value;
links[index].tags = modal.querySelector('.ulm-edit-tags').value.split(',').map(t => t.trim()).filter(Boolean);
links[index].folder = modal.querySelector('.ulm-edit-folder').value.trim();
await saveLinks(links);
modal.remove();
await refreshUI();
}
};
modal.querySelector('.ulm-cancel-edit').onclick = () => modal.remove();
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
}
// --- Populate Exclude List ---
function populateExcludeList(domains, excludeListElement) {
excludeListElement.innerHTML = '';
if (domains.length === 0) {
excludeListElement.innerHTML = '<div class="ulm-empty-state" style="padding:15px;"><div class="ulm-empty-state-icon" style="font-size:24px;">🌐</div><div class="ulm-empty-state-subtitle">No excluded domains</div></div>';
return;
}
domains.forEach((domain, index) => {
const domainWrapper = document.createElement('div');
domainWrapper.className = 'ulm-exclude-wrapper';
domainWrapper.innerHTML = `<span>${domain}</span>`;
if (isExcludeDeleteMode) {
const deleteButton = document.createElement('button');
deleteButton.className = 'ulm-action-btn ulm-delete-btn';
deleteButton.textContent = '×';
deleteButton.onclick = async () => {
domains.splice(index, 1);
await saveExcludedDomains(domains);
populateExcludeList(domains, excludeListElement);
};
domainWrapper.appendChild(deleteButton);
}
excludeListElement.appendChild(domainWrapper);
});
}
// --- Render a Generic Manager ---
function renderManager(type, items, container) {
container.innerHTML = '';
items.forEach((item, index) => {
const isCategory = type === 'category';
const name = isCategory ? item.name : item;
const itemEl = document.createElement('div');
itemEl.className = 'ulm-manager-item';
let colorInput = '';
if (isCategory) {
colorInput = `<input type="color" class="item-color" value="${item.color}" title="Change color">`;
}
itemEl.innerHTML = `
${colorInput}
<input type="text" class="item-name" value="${name}" readonly>
<div class="item-actions">
<button class="item-btn rename" title="Rename">✎</button>
<button class="item-btn delete" title="Delete">×</button>
</div>
`;
container.appendChild(itemEl);
if (isCategory) {
itemEl.querySelector('.item-color').onchange = (e) => handleCategoryColorChange(index, e.target.value);
}
const nameInput = itemEl.querySelector('.item-name');
const renameAction = () => {
nameInput.readOnly = false;
nameInput.classList.add('editing');
nameInput.focus();
nameInput.select();
};
itemEl.querySelector('.rename').onclick = renameAction;
const saveRename = async () => {
if (!nameInput.readOnly) {
const newName = nameInput.value.trim();
nameInput.readOnly = true;
nameInput.classList.remove('editing');
if (isCategory) {
await handleRenameCategory(index, newName);
} else {
await handleRenameFolder(name, newName);
}
}
};
nameInput.onblur = saveRename;
nameInput.onkeydown = (e) => {
if (e.key === 'Enter') saveRename();
else if (e.key === 'Escape') {
nameInput.value = name;
nameInput.readOnly = true;
nameInput.classList.remove('editing');
}
};
itemEl.querySelector('.delete').onclick = () => {
if (isCategory) handleDeleteCategory(index);
else handleDeleteFolder(name);
};
});
}
// --- Category Management Handlers ---
async function handleCategoryColorChange(index, newColor) {
const categories = await getCategories();
if(categories[index]) {
categories[index].color = newColor;
await saveCategories(categories);
await refreshUI();
}
}
async function handleRenameCategory(index, newName) {
if (!newName) { showToast('Category name cannot be empty!', 'error'); await refreshUI(); return; }
const categories = await getCategories();
const oldName = categories[index].name;
if (newName.toLowerCase() === oldName.toLowerCase()) { await refreshUI(); return; }
if (categories.some((c, i) => i !== index && c.name.toLowerCase() === newName.toLowerCase())) {
showToast('A category with this name already exists!', 'error');
await refreshUI();
return;
}
categories[index].name = newName.toLowerCase();
await saveCategories(categories);
const links = await getLinks();
links.forEach(link => { if (link.category === oldName) link.category = newName.toLowerCase(); });
await saveLinks(links);
if (currentCategory === oldName) currentCategory = newName.toLowerCase();
await refreshUI();
}
async function handleDeleteCategory(index) {
const categories = await getCategories();
const catToDelete = categories[index];
if (catToDelete.name === 'default') { showToast('Cannot delete the default category!', 'warning'); return; }
if (confirm(`Delete category "${catToDelete.name}"? Links using it will be moved to "default".`)) {
categories.splice(index, 1);
await saveCategories(categories);
const links = await getLinks();
links.forEach(link => { if (link.category === catToDelete.name) link.category = 'default'; });
await saveLinks(links);
if (currentCategory === catToDelete.name) currentCategory = 'all';
await refreshUI();
}
}
// --- Folder Management Handlers ---
async function handleRenameFolder(oldName, newName) {
if (!newName) { showToast('Folder name cannot be empty!', 'error'); await refreshUI(); return; }
const folders = await getFolders();
if (newName.toLowerCase() === oldName.toLowerCase()) { await refreshUI(); return; }
if (folders.some(f => f.toLowerCase() === newName.toLowerCase())) {
showToast('A folder with this name already exists!', 'error');
await refreshUI();
return;
}
const folderIndex = folders.findIndex(f => f === oldName);
if (folderIndex > -1) {
folders[folderIndex] = newName;
await saveFolders(folders);
const links = await getLinks();
links.forEach(link => { if (link.folder === oldName) link.folder = newName; });
await saveLinks(links);
if (currentFolder === oldName) currentFolder = newName;
await refreshUI();
}
}
async function handleDeleteFolder(folderName) {
if (confirm(`Delete folder "${folderName}"? Links will be removed from this folder but not deleted.`)) {
let folders = await getFolders();
folders = folders.filter(f => f !== folderName);
await saveFolders(folders);
const links = await getLinks();
links.forEach(link => { if (link.folder === folderName) link.folder = ''; });
await saveLinks(links);
if (currentFolder === folderName) currentFolder = '';
await refreshUI();
}
}
// --- Initialize Category Select ---
async function initializeCategorySelect(categories) {
const categorySelect = shadowRoot.querySelector('.ulm-link-category-select');
if (categorySelect) {
categorySelect.innerHTML = categories.map(cat => `<option value="${cat.name}">${cat.name.charAt(0).toUpperCase() + cat.name.slice(1)}</option>`).join('');
}
}
// --- Apply Button Position ---
function applyButtonPosition(position) {
[bubble, showBubbleButton, bubbleMenu].forEach(el => {
if (el) {
el.style.top = el.style.left = el.style.bottom = el.style.right = 'auto';
el.style[position.vertical] = '30px';
el.style[position.horizontal] = '30px';
if (el === bubbleMenu) {
el.style[position.vertical] = '100px';
}
}
});
shadowRoot.querySelectorAll('.ulm-position-grid .ulm-btn').forEach(btn => btn.classList.remove('active'));
const activeBtn = shadowRoot.querySelector(`#position-${position.vertical}-${position.horizontal}`);
if (activeBtn) activeBtn.classList.add('active');
}
// --- Create Main UI HTML ---
function createMainUIHTML() {
return `
<div class="ulm-layout-header">
<div class="ulm-header-left">
<h2>Universal Links Pro</h2>
<span class="ulm-header-info ulm-link-count"></span>
<span class="ulm-header-shortcuts"><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>U</kbd> toggle · <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd> quick add</span>
</div>
<button class="ulm-close-btn">×</button>
</div>
<div class="ulm-layout-body">
<div class="ulm-sidebar">
<div class="ulm-sidebar-header">
<span>📁 Folders</span>
<button class="ulm-sidebar-toggle" title="Collapse sidebar">◀</button>
</div>
<div class="ulm-folder-nav"></div>
</div>
<button class="ulm-sidebar-expand-btn" title="Show folders">▶</button>
<div class="ulm-content">
<div class="ulm-search-container">
<input type="text" class="ulm-search-input" placeholder="Search links, tags, folders..."><button class="ulm-clear-search">✕</button>
</div>
<div class="ulm-sort-filter-bar">
<div class="ulm-category-filter"><label>Filter:</label><select class="ulm-category-dropdown"></select></div>
<select class="ulm-sort-dropdown"><option value="manual">Manual Order</option><option value="az">A → Z</option><option value="za">Z → A</option><option value="recent">Newest First</option></select>
<button class="ulm-recent-btn">🕐 Recent</button>
</div>
<div class="ulm-alpha-bar"></div>
<div class="ulm-recent-section"><div class="ulm-recent-header"><h3>🕐 Recently Added</h3><span class="ulm-recent-count"></span></div><div class="ulm-recent-list"></div></div>
<div class="ulm-link-list" tabindex="0"></div>
<div class="ulm-accordion">
<button class="ulm-accordion-header" data-section="addLink">
<span>➕ Add Link</span>
<span class="ulm-accordion-arrow">▼</span>
</button>
<div class="ulm-accordion-body" data-content="addLink">
<div class="ulm-form-row"><div class="ulm-form-group"><label>Label</label><input type="text" class="ulm-link-label-input" placeholder="My Site"></div><div class="ulm-form-group"><label>Category</label><select class="ulm-link-category-select"></select></div></div>
<div class="ulm-form-group"><label>URL</label><input type="text" class="ulm-link-url-input" placeholder="https://example.com"></div>
<div class="ulm-form-group"><label>Tags (press Enter to add)</label><div class="ulm-tag-input-wrapper ulm-add-tags-wrapper"><input type="text" class="ulm-tag-input" placeholder="Add tag..."></div></div>
<div class="ulm-form-group"><label>Folder (optional)</label><input type="text" class="ulm-link-folder-input" placeholder="e.g. Projects/AI" list="ulm-folder-suggestions"><datalist id="ulm-folder-suggestions"></datalist></div>
<div class="ulm-btn-group" style="margin-top:8px;"><button class="ulm-btn ulm-btn-primary ulm-save-link-btn">Save Link</button><button class="ulm-btn ulm-delete-links-btn">Edit Mode</button></div>
<div class="ulm-form-group" style="margin-top: 10px;"><label>Add New Category</label><div class="ulm-form-row"><input type="text" class="ulm-new-category-input" placeholder="New category name" style="flex: 2;"><button class="ulm-btn ulm-add-category-btn" style="flex: 1;">Add</button></div></div>
</div>
<button class="ulm-accordion-header" data-section="appearance">
<span>🎨 Appearance</span>
<span class="ulm-accordion-arrow">▼</span>
</button>
<div class="ulm-accordion-body" data-content="appearance">
<div class="ulm-form-group"><label>Theme</label><div class="ulm-theme-grid"></div></div>
<div class="ulm-custom-color-section" style="display: none;"><div class="ulm-form-group"><label>Custom Colors</label><div class="ulm-color-grid"></div></div></div>
<div class="ulm-form-group" style="margin-top:10px;"><label>Bubble Icon</label><input type="text" class="ulm-bubble-icon-input" maxlength="2" style="width: 70px; text-align: center; font-size: 24px;"></div>
<div class="ulm-form-group"><label>Bubble Size: <span class="ulm-bubble-size-value">60</span>px</label><div class="ulm-range-container"><input type="range" class="ulm-bubble-size-slider" min="40" max="100" value="60"></div></div>
<div class="ulm-form-group"><label>Button Position</label><div class="ulm-position-grid"><button id="position-top-left" class="ulm-btn">↖</button><button id="position-top-right" class="ulm-btn">↗</button><button id="position-bottom-left" class="ulm-btn">↙</button><button id="position-bottom-right" class="ulm-btn">↘</button></div></div>
</div>
<button class="ulm-accordion-header" data-section="settings">
<span>⚙️ Settings</span>
<span class="ulm-accordion-arrow">▼</span>
</button>
<div class="ulm-accordion-body" data-content="settings">
<div class="ulm-settings-section">
<div class="ulm-settings-section-header open" data-group="display"><span>🖥️ Display</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body open" data-group-body="display">
<div class="ulm-settings-grid">
<div class="ulm-setting-item"><label>Animations</label><label class="ulm-toggle-switch"><input type="checkbox" class="ulm-toggle-animations"><span class="ulm-toggle-slider"></span></label></div>
<div class="ulm-setting-item"><label>Show Favicons</label><label class="ulm-toggle-switch"><input type="checkbox" class="ulm-toggle-favicons"><span class="ulm-toggle-slider"></span></label></div>
<div class="ulm-setting-item"><label>Show Click Count</label><label class="ulm-toggle-switch"><input type="checkbox" class="ulm-toggle-click-count"><span class="ulm-toggle-slider"></span></label></div>
</div>
</div>
</div>
<div class="ulm-settings-section">
<div class="ulm-settings-section-header" data-group="behavior"><span>⚡ Behavior</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body" data-group-body="behavior">
<div class="ulm-settings-grid">
<div class="ulm-setting-item"><label>Open in New Tab</label><label class="ulm-toggle-switch"><input type="checkbox" class="ulm-toggle-new-tab"><span class="ulm-toggle-slider"></span></label></div>
<div class="ulm-setting-item"><label>Confirm Delete</label><label class="ulm-toggle-switch"><input type="checkbox" class="ulm-toggle-confirm-delete"><span class="ulm-toggle-slider"></span></label></div>
</div>
<div class="ulm-form-group" style="margin-top: 10px;"><label>Recent Links Count: <span class="ulm-recent-count-value">10</span></label><div class="ulm-range-container"><input type="range" class="ulm-recent-count-slider" min="3" max="25" value="10"></div></div>
</div>
</div>
<div class="ulm-settings-section">
<div class="ulm-settings-section-header" data-group="panelDefaults"><span>📋 Panel Defaults</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body" data-group-body="panelDefaults">
<p style="font-size:11px; color:inherit; opacity:0.6; margin:0 0 8px 0;">Choose which sections open by default when you launch the panel.</p>
<div class="ulm-defaults-grid">
<label class="ulm-default-section-item"><input type="checkbox" value="addLink" class="ulm-default-section-check"><span>➕ Add Link</span></label>
<label class="ulm-default-section-item"><input type="checkbox" value="appearance" class="ulm-default-section-check"><span>🎨 Appearance</span></label>
<label class="ulm-default-section-item"><input type="checkbox" value="settings" class="ulm-default-section-check"><span>⚙️ Settings</span></label>
<label class="ulm-default-section-item"><input type="checkbox" value="backup" class="ulm-default-section-check"><span>💾 Backup & Tools</span></label>
</div>
</div>
</div>
<div class="ulm-settings-section">
<div class="ulm-settings-section-header" data-group="categories"><span>🏷️ Categories</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body" data-group-body="categories">
<div class="ulm-category-manager ulm-manager-list"></div>
</div>
</div>
<div class="ulm-settings-section">
<div class="ulm-settings-section-header" data-group="excludes"><span>🚫 Excluded Domains</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body" data-group-body="excludes">
<div class="ulm-exclude-list"></div>
<div class="ulm-form-row" style="margin-top: 5px;"><input type="text" class="ulm-exclude-url-input" placeholder="example.com" style="flex: 2;"><button class="ulm-btn ulm-save-exclude-btn" style="flex: 1;">Add</button></div>
<button class="ulm-btn ulm-delete-exclude-btn" style="margin-top: 5px; width: 100%;">Manage Excludes</button>
</div>
</div>
<div class="ulm-settings-section">
<div class="ulm-settings-section-header" data-group="danger"><span>⚠️ Danger Zone</span><span class="ulm-ss-arrow">▼</span></div>
<div class="ulm-settings-section-body" data-group-body="danger">
<div class="ulm-btn-group"><button class="ulm-btn ulm-hide-btn">Hide Bubble</button><button class="ulm-btn ulm-btn-danger ulm-reset-btn">Reset All</button></div>
</div>
</div>
</div>
<button class="ulm-accordion-header" data-section="backup">
<span>💾 Backup & Tools</span>
<span class="ulm-accordion-arrow">▼</span>
</button>
<div class="ulm-accordion-body" data-content="backup">
<div class="ulm-form-group"><label>Export/Import</label><p style="font-size: 12px; color: #888; margin: 5px 0;">Includes links, settings, themes, and categories.</p><div class="ulm-btn-group"><button class="ulm-btn ulm-export-all-btn">Export All</button><button class="ulm-btn ulm-copy-export-btn" style="display: none;">Copy</button></div><textarea class="ulm-export-area ulm-backup-area" style="display: none; margin-top: 10px;" readonly></textarea></div>
<div class="ulm-form-group" style="margin-top: 15px;"><textarea class="ulm-import-area ulm-backup-area" placeholder="Paste your backup data here..."></textarea><div class="ulm-btn-group" style="margin-top: 5px;"><button class="ulm-btn ulm-import-links-btn">Import Links Only</button><button class="ulm-btn ulm-btn-primary ulm-import-all-btn">Import All</button></div></div>
<div class="ulm-form-group" style="margin-top: 15px;"><label>Quick Actions</label><div class="ulm-btn-group"><button class="ulm-btn ulm-export-links-only-btn">Export Links Only</button><button class="ulm-btn ulm-clear-stats-btn">Clear Click Stats</button></div><button class="ulm-btn ulm-health-check-btn" style="margin-top: 8px; width: 100%;">🏥 Check All Links Health</button></div>
<div class="ulm-form-group" style="margin-top: 15px;"><label>Folder Management</label><div class="ulm-folder-manager ulm-manager-list"></div><div class="ulm-form-row" style="margin-top: 5px;"><input type="text" class="ulm-new-folder-input" placeholder="New folder name" style="flex: 2;"><button class="ulm-btn ulm-add-folder-btn" style="flex: 1;">Add Folder</button></div></div>
</div>
</div>
</div>
</div>`;
}
// --- Build Sidebar Folder Navigation ---
async function buildFolderSidebar(links) {
const folderNav = shadowRoot.querySelector('.ulm-folder-nav');
if (!folderNav) return;
folderNav.innerHTML = '';
// "All Links" item
const allItem = document.createElement('button');
allItem.className = 'ulm-folder-nav-item' + (!currentFolder ? ' active' : '');
allItem.innerHTML = `<span class="folder-icon">📁</span><span class="folder-name">All Links</span><span class="folder-count">${links.length}</span>`;
allItem.onclick = async () => { currentFolder = ''; await refreshUI(); };
folderNav.appendChild(allItem);
// Uncategorized count
const uncategorizedCount = links.filter(l => !l.folder).length;
if (uncategorizedCount > 0 && uncategorizedCount < links.length) {
const uncatItem = document.createElement('button');
uncatItem.className = 'ulm-folder-nav-item' + (currentFolder === '__uncategorized__' ? ' active' : '');
uncatItem.innerHTML = `<span class="folder-icon">📄</span><span class="folder-name">Unfiled</span><span class="folder-count">${uncategorizedCount}</span>`;
uncatItem.onclick = async () => { currentFolder = (currentFolder === '__uncategorized__') ? '' : '__uncategorized__'; await refreshUI(); };
folderNav.appendChild(uncatItem);
}
// Divider
const folderCounts = links.reduce((acc, link) => {
if (link.folder) acc[link.folder] = (acc[link.folder] || 0) + 1;
return acc;
}, {});
const savedFolders = await getFolders();
const allFolderNames = new Set([...savedFolders, ...Object.keys(folderCounts)]);
if (allFolderNames.size > 0) {
const divider = document.createElement('div');
divider.className = 'ulm-sidebar-divider';
folderNav.appendChild(divider);
}
// Folder items
[...allFolderNames].sort((a, b) => a.localeCompare(b)).forEach(folderName => {
const count = folderCounts[folderName] || 0;
const item = document.createElement('button');
item.className = 'ulm-folder-nav-item' + (currentFolder === folderName ? ' active' : '') + (isDeleteMode ? ' edit-mode' : '');
item.innerHTML = `
<span class="folder-icon">📂</span>
<span class="folder-name">${folderName}</span>
<span class="folder-count">${count}</span>
<button class="folder-delete-btn" title="Delete folder">×</button>
`;
item.onclick = async (e) => {
if (e.target.classList.contains('folder-delete-btn')) return;
currentFolder = (currentFolder === folderName) ? '' : folderName;
await refreshUI();
};
const deleteBtn = item.querySelector('.folder-delete-btn');
deleteBtn.onclick = async (e) => {
e.stopPropagation();
if (confirm(`Delete folder "${folderName}"? Links will be removed from this folder but not deleted.`)) {
let folders = await getFolders();
folders = folders.filter(f => f !== folderName);
await saveFolders(folders);
const allLinks = await getLinks();
allLinks.forEach(link => { if (link.folder === folderName) link.folder = ''; });
await saveLinks(allLinks);
if (currentFolder === folderName) currentFolder = '';
await refreshUI();
}
};
folderNav.appendChild(item);
});
}
// --- Open UI ---
async function openUI() {
// Apply default open sections on fresh open
const settings = await getSettings();
const defaults = settings.defaultOpenSections || [];
// Reset all accordion states
shadowRoot.querySelectorAll('.ulm-accordion-header').forEach(h => h.classList.remove('active'));
shadowRoot.querySelectorAll('.ulm-accordion-body').forEach(b => b.classList.remove('open'));
// Open the defaults
defaults.forEach(section => {
const header = shadowRoot.querySelector(`.ulm-accordion-header[data-section="${section}"]`);
const body = shadowRoot.querySelector(`.ulm-accordion-body[data-content="${section}"]`);
if (header && body) {
header.classList.add('active');
body.classList.add('open');
}
});
openAccordion = defaults.length > 0 ? defaults[0] : '';
await refreshUI();
mainUI.classList.add('visible');
}
// --- Refresh UI ---
async function refreshUI() {
const links = await getLinks();
const categories = await getCategories();
const settings = await getSettings();
const currentTheme = await getTheme();
const customColors = await getCustomColors();
const excluded = await getExcludedDomains();
const clickStats = await getClickStats();
const folders = await getFolders();
// Update link count
const linkCountEl = shadowRoot.querySelector('.ulm-link-count');
if (linkCountEl) linkCountEl.textContent = `${links.length} links saved`;
// Handle __uncategorized__ filter
let effectiveLinks = links;
let realCurrentFolder = currentFolder;
if (currentFolder === '__uncategorized__') {
realCurrentFolder = '';
// We'll handle this in populateLinkList by filtering unfiled
}
// Populate UI components
await initializeCategorySelect(categories);
await populateCategoryDropdown(links, categories);
await buildAlphaBar(links);
await buildFolderSidebar(links);
await populateRecentSection(links, settings, categories);
// Special handling for uncategorized folder filter
if (currentFolder === '__uncategorized__') {
const origCurrentFolder = currentFolder;
currentFolder = '';
const unfiledLinks = links.filter(l => !l.folder);
await populateLinkListCustom(unfiledLinks, links, settings, categories, clickStats);
currentFolder = origCurrentFolder;
} else {
await populateLinkList(links, settings, categories, clickStats);
}
renderManager('category', categories, shadowRoot.querySelector('.ulm-category-manager'));
renderManager('folder', folders, shadowRoot.querySelector('.ulm-folder-manager'));
populateExcludeList(excluded, shadowRoot.querySelector('.ulm-exclude-list'));
// Update settings values
const sortDropdown = shadowRoot.querySelector('.ulm-sort-dropdown');
if (sortDropdown) sortDropdown.value = currentSortMode;
shadowRoot.querySelector('.ulm-bubble-icon-input').value = settings.bubbleIcon;
shadowRoot.querySelector('.ulm-bubble-size-slider').value = settings.bubbleSize;
shadowRoot.querySelector('.ulm-bubble-size-value').textContent = settings.bubbleSize;
shadowRoot.querySelector('.ulm-recent-count-slider').value = settings.recentCount;
shadowRoot.querySelector('.ulm-recent-count-value').textContent = settings.recentCount;
// Toggles
shadowRoot.querySelector('.ulm-toggle-animations').checked = settings.animationsEnabled;
shadowRoot.querySelector('.ulm-toggle-new-tab').checked = settings.openInNewTab;
shadowRoot.querySelector('.ulm-toggle-click-count').checked = settings.showClickCount;
shadowRoot.querySelector('.ulm-toggle-confirm-delete').checked = settings.confirmDelete;
shadowRoot.querySelector('.ulm-toggle-favicons').checked = settings.showFavicons;
// Default open sections checkboxes
const defaultChecks = shadowRoot.querySelectorAll('.ulm-default-section-check');
defaultChecks.forEach(check => {
check.checked = (settings.defaultOpenSections || []).includes(check.value);
});
// Sidebar collapsed state
const sidebar = shadowRoot.querySelector('.ulm-sidebar');
const expandBtn = shadowRoot.querySelector('.ulm-sidebar-expand-btn');
if (sidebar) sidebar.classList.toggle('collapsed', settings.sidebarCollapsed);
if (expandBtn) expandBtn.classList.toggle('visible', settings.sidebarCollapsed);
// Populate folder datalist
const folderDatalist = shadowRoot.querySelector('#ulm-folder-suggestions');
if(folderDatalist) folderDatalist.innerHTML = folders.map(f => `<option value="${f}"></option>`).join('');
// Populate theme grid
const themeGrid = shadowRoot.querySelector('.ulm-theme-grid');
if (themeGrid) {
themeGrid.innerHTML = Object.entries(themes).map(([key, theme]) => {
const colors = key === 'custom' ? customColors : theme.colors;
return `<div class="ulm-theme-option ${currentTheme === key ? 'active' : ''}" data-theme="${key}">
<div class="ulm-theme-preview" style="background: linear-gradient(135deg, ${colors.bubbleBackground} 0%, ${colors.menuBackground} 100%); border: 1px solid ${colors.menuBorder};"></div>
<span>${theme.name}</span>
</div>`;
}).join('');
}
// Custom colors
const colorGrid = shadowRoot.querySelector('.ulm-color-grid');
if (colorGrid) {
colorGrid.innerHTML = Object.entries(customColors).map(([key, value]) => `
<div class="ulm-color-item">
<label>${key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</label>
<input type="color" data-color-key="${key}" value="${value}">
</div>`).join('');
}
shadowRoot.querySelector('.ulm-custom-color-section').style.display = currentTheme === 'custom' ? 'block' : 'none';
}
// Special version for unfiled links display
async function populateLinkListCustom(filteredSourceLinks, allLinks, settings, categories, clickStats) {
const linkListElement = shadowRoot.querySelector('.ulm-link-list');
linkListElement.innerHTML = '';
let filteredLinks = filteredSourceLinks;
if (currentCategory !== 'all') {
filteredLinks = filteredLinks.filter(link => link.category === currentCategory);
}
if (searchQuery) {
const searchWords = searchQuery.toLowerCase().split(' ').filter(w => w);
filteredLinks = filteredLinks.filter(link => {
const searchableText = [link.label, link.url, ...(link.tags || []), link.folder || ''].join(' ').toLowerCase();
return searchWords.every(word => searchableText.includes(word));
});
}
if (activeLetter) {
filteredLinks = filteredLinks.filter(link => {
const firstChar = link.label.charAt(0).toUpperCase();
return activeLetter === '#' ? !/[A-Z]/.test(firstChar) : firstChar === activeLetter;
});
}
filteredLinks = sortLinks(filteredLinks, currentSortMode);
if (filteredLinks.length === 0) {
linkListElement.innerHTML = '<div class="ulm-empty-state"><div class="ulm-empty-state-icon">📂</div><div class="ulm-empty-state-title">No unfiled links</div><div class="ulm-empty-state-subtitle">All your links are in folders</div></div>';
return;
}
// Reuse the same rendering logic but with the filtered source
const showGroups = (currentSortMode === 'az' || currentSortMode === 'za') && !activeLetter && !searchQuery;
let lastLetter = null;
filteredLinks.forEach((linkData) => {
const originalIndex = allLinks.indexOf(linkData);
if (showGroups) {
const firstChar = linkData.label.charAt(0).toUpperCase();
const groupLetter = /[A-Z]/.test(firstChar) ? firstChar : '#';
if (groupLetter !== lastLetter) {
lastLetter = groupLetter;
const groupCount = filteredLinks.filter(l => (l.label.charAt(0).toUpperCase() === groupLetter) || (groupLetter === '#' && !/[A-Z]/.test(l.label.charAt(0).toUpperCase()))).length;
const header = document.createElement('div');
header.className = 'ulm-letter-group-header';
header.innerHTML = `<span class="ulm-letter-group-char">${groupLetter}</span><div class="ulm-letter-group-line"></div><span class="ulm-letter-group-count">${groupCount}</span>`;
linkListElement.appendChild(header);
}
}
const linkWrapper = document.createElement('div');
linkWrapper.className = 'ulm-link-wrapper';
linkWrapper.dataset.index = originalIndex;
const link = document.createElement('a');
link.href = linkData.url;
link.target = settings.openInNewTab ? '_blank' : '_self';
link.className = 'ulm-link';
const linkContent = document.createElement('div');
linkContent.className = 'ulm-link-content';
const healthDot = document.createElement('div');
healthDot.className = `ulm-health-dot ${linkData.health.status || 'unknown'}`;
linkContent.appendChild(healthDot);
if (settings.showFavicons) {
const favicon = document.createElement('img');
favicon.className = 'ulm-favicon';
favicon.src = getFaviconUrl(linkData.url);
favicon.onerror = () => { favicon.style.display = 'none'; };
linkContent.appendChild(favicon);
}
const labelSpan = document.createElement('span');
labelSpan.className = 'ulm-link-label';
labelSpan.textContent = linkData.label;
linkContent.appendChild(labelSpan);
link.appendChild(linkContent);
const metaSpan = document.createElement('span');
metaSpan.className = 'ulm-link-meta';
link.appendChild(metaSpan);
link.addEventListener('click', () => { if (!isDeleteMode) incrementClickStat(linkData.url); });
linkWrapper.appendChild(link);
linkListElement.appendChild(linkWrapper);
});
}
// --- Setup Event Listeners ---
function setupEventListeners() {
// Keyboard shortcut to toggle UI
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.altKey && (event.key === 'u' || event.key === 'U')) {
event.preventDefault();
const isVisible = mainUI.classList.contains('visible');
if (isVisible) mainUI.classList.remove('visible');
else openUI();
}
if (event.ctrlKey && event.altKey && (event.key === 'a' || event.key === 'A')) {
event.preventDefault();
instantAddCurrentPage();
}
});
// Bubble interactions
bubble.addEventListener('click', (e) => {
e.stopPropagation();
bubbleMenu.classList.toggle('visible');
});
let bubbleClickCount = 0, bubbleClickTimer = null;
bubble.addEventListener('click', () => {
bubbleClickCount++;
if (bubbleClickTimer) clearTimeout(bubbleClickTimer);
bubbleClickTimer = setTimeout(() => bubbleClickCount = 0, 300);
if (bubbleClickCount === 3) {
clearTimeout(bubbleClickTimer);
bubble.classList.add('hidden');
showBubbleButton.classList.add('visible');
mainUI.classList.remove('visible');
bubbleMenu.classList.remove('visible');
saveBubbleHiddenState(true);
bubbleClickCount = 0;
}
});
showBubbleButton.addEventListener('click', () => {
bubble.classList.remove('hidden');
showBubbleButton.classList.remove('visible');
saveBubbleHiddenState(false);
});
shadowRoot.querySelector('.ulm-instant-add-btn').onclick = async () => {
await instantAddCurrentPage();
};
shadowRoot.querySelector('.ulm-show-menu-btn').onclick = () => {
bubbleMenu.classList.remove('visible');
openUI();
};
shadowRoot.querySelector('.ulm-close-btn').onclick = () => mainUI.classList.remove('visible');
// Sidebar toggle
shadowRoot.querySelector('.ulm-sidebar-toggle').onclick = async () => {
const settings = await getSettings();
settings.sidebarCollapsed = true;
await saveSettings(settings);
await refreshUI();
};
shadowRoot.querySelector('.ulm-sidebar-expand-btn').onclick = async () => {
const settings = await getSettings();
settings.sidebarCollapsed = false;
await saveSettings(settings);
await refreshUI();
};
// Link List Keyboard Navigation
const linkList = shadowRoot.querySelector('.ulm-link-list');
linkList.addEventListener('keydown', (e) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Enter') return;
e.preventDefault();
const items = Array.from(linkList.querySelectorAll('.ulm-link-wrapper'));
if (items.length === 0) return;
let currentIndex = items.findIndex(item => item.classList.contains('selected'));
if (e.key === 'Enter' && currentIndex > -1) {
items[currentIndex].querySelector('.ulm-link')?.click();
return;
}
if(currentIndex > -1) items[currentIndex].classList.remove('selected');
if (e.key === 'ArrowDown') currentIndex = (currentIndex + 1) % items.length;
else if (e.key === 'ArrowUp') currentIndex = (currentIndex - 1 + items.length) % items.length;
items[currentIndex].classList.add('selected');
items[currentIndex].scrollIntoView({ block: 'nearest' });
});
// Filtering and sorting
const searchInput = shadowRoot.querySelector('.ulm-search-input');
searchInput.oninput = async () => { searchQuery = searchInput.value; await refreshUI(); };
shadowRoot.querySelector('.ulm-clear-search').onclick = async () => { searchInput.value = ''; searchQuery = ''; await refreshUI(); };
shadowRoot.querySelector('.ulm-category-dropdown').onchange = async (e) => { currentCategory = e.target.value; activeLetter = null; await refreshUI(); };
shadowRoot.querySelector('.ulm-sort-dropdown').onchange = async (e) => {
currentSortMode = e.target.value;
activeLetter = null;
const settings = await getSettings();
settings.sortMode = currentSortMode;
await saveSettings(settings);
await refreshUI();
};
shadowRoot.querySelector('.ulm-recent-btn').onclick = async () => {
showRecentlyAdded = !showRecentlyAdded;
shadowRoot.querySelector('.ulm-recent-btn').classList.toggle('active', showRecentlyAdded);
await refreshUI();
};
// Accordion Sections
shadowRoot.querySelectorAll('.ulm-accordion-header').forEach(header => {
header.addEventListener('click', () => {
const section = header.dataset.section;
const body = shadowRoot.querySelector(`.ulm-accordion-body[data-content="${section}"]`);
const isOpen = header.classList.contains('active');
// Close all
shadowRoot.querySelectorAll('.ulm-accordion-header').forEach(h => h.classList.remove('active'));
shadowRoot.querySelectorAll('.ulm-accordion-body').forEach(b => b.classList.remove('open'));
// Toggle
if (!isOpen) {
header.classList.add('active');
body.classList.add('open');
openAccordion = section;
} else {
openAccordion = '';
}
});
});
// Settings collapsible sections
shadowRoot.querySelectorAll('.ulm-settings-section-header').forEach(header => {
header.addEventListener('click', () => {
const group = header.dataset.group;
const body = shadowRoot.querySelector(`.ulm-settings-section-body[data-group-body="${group}"]`);
const isOpen = header.classList.contains('open');
header.classList.toggle('open', !isOpen);
body.classList.toggle('open', !isOpen);
});
});
// Default open sections checkboxes
shadowRoot.querySelector('.ulm-defaults-grid').addEventListener('change', async (e) => {
if (!e.target.classList.contains('ulm-default-section-check')) return;
const settings = await getSettings();
const checks = shadowRoot.querySelectorAll('.ulm-default-section-check');
settings.defaultOpenSections = Array.from(checks).filter(c => c.checked).map(c => c.value);
await saveSettings(settings);
});
// Add Link Form
const addTagsWrapper = shadowRoot.querySelector('.ulm-add-tags-wrapper');
const tagInput = addTagsWrapper.querySelector('.ulm-tag-input');
let addLinkTags = [];
tagInput.onkeydown = (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const tag = tagInput.value.trim().replace(',', '');
if (tag && !addLinkTags.includes(tag)) {
addLinkTags.push(tag);
const tagEl = document.createElement('span');
tagEl.className = 'ulm-tag';
tagEl.textContent = tag;
tagEl.onclick = () => { addLinkTags = addLinkTags.filter(t => t !== tag); tagEl.remove(); };
addTagsWrapper.insertBefore(tagEl, tagInput);
}
tagInput.value = '';
}
};
const urlInput = shadowRoot.querySelector('.ulm-link-url-input');
urlInput.onblur = async () => {
const catSelect = shadowRoot.querySelector('.ulm-link-category-select');
if (urlInput.value.trim() && catSelect.value === 'default') {
const suggested = await suggestCategory(urlInput.value.trim());
if (suggested !== 'default') catSelect.value = suggested;
}
};
shadowRoot.querySelector('.ulm-save-link-btn').onclick = async () => {
const label = shadowRoot.querySelector('.ulm-link-label-input').value.trim();
const url = shadowRoot.querySelector('.ulm-link-url-input').value.trim();
if (!label || !url) { showToast('Label and URL are required.', 'error'); return; }
if (await checkDuplicate(url)) {
if (!confirm('This URL already exists. Add anyway?')) return;
}
const links = await getLinks();
const folderValue = shadowRoot.querySelector('.ulm-link-folder-input').value.trim();
links.push({
label, url,
category: shadowRoot.querySelector('.ulm-link-category-select').value,
folder: folderValue,
tags: addLinkTags,
addedAt: Date.now(),
health: { status: 'unknown', checkedAt: 0 }
});
await saveLinks(links);
// Auto-add folder if new
if (folderValue) {
const folders = await getFolders();
if (!folders.some(f => f.toLowerCase() === folderValue.toLowerCase())) {
folders.push(folderValue);
await saveFolders(folders);
}
}
shadowRoot.querySelector('.ulm-link-label-input').value = '';
shadowRoot.querySelector('.ulm-link-url-input').value = '';
shadowRoot.querySelector('.ulm-link-folder-input').value = '';
tagInput.value = '';
addTagsWrapper.querySelectorAll('.ulm-tag').forEach(t => t.remove());
addLinkTags = [];
showToast(`"${label}" saved!`);
await refreshUI();
};
shadowRoot.querySelector('.ulm-delete-links-btn').onclick = async () => {
isDeleteMode = !isDeleteMode;
mainUI.classList.toggle('edit-mode', isDeleteMode);
shadowRoot.querySelector('.ulm-delete-links-btn').textContent = isDeleteMode ? 'Exit Edit Mode' : 'Edit Mode';
await refreshUI();
};
shadowRoot.querySelector('.ulm-add-category-btn').onclick = async () => {
const input = shadowRoot.querySelector('.ulm-new-category-input');
const newCatName = input.value.trim().toLowerCase();
if (!newCatName) return;
const categories = await getCategories();
if (categories.some(c => c.name === newCatName)) { showToast('Category already exists!', 'warning'); return; }
categories.push({ name: newCatName, color: generateRandomColor() });
await saveCategories(categories);
input.value = '';
await refreshUI();
};
// Appearance Tab
shadowRoot.querySelector('.ulm-theme-grid').onclick = async (e) => {
const themeOption = e.target.closest('.ulm-theme-option');
if (!themeOption) return;
const themeName = themeOption.dataset.theme;
await saveTheme(themeName);
await applyTheme(themeName);
await refreshUI();
};
shadowRoot.querySelector('.ulm-color-grid').oninput = async (e) => {
if (e.target.type !== 'color') return;
const colors = await getCustomColors();
colors[e.target.dataset.colorKey] = e.target.value;
await saveCustomColors(colors);
if (await getTheme() === 'custom') await applyTheme('custom');
};
shadowRoot.querySelector('.ulm-bubble-icon-input').oninput = async (e) => {
const settings = await getSettings();
settings.bubbleIcon = e.target.value || 'λ';
await saveSettings(settings);
bubble.textContent = settings.bubbleIcon;
};
shadowRoot.querySelector('.ulm-bubble-size-slider').oninput = async (e) => {
const size = parseInt(e.target.value);
shadowRoot.querySelector('.ulm-bubble-size-value').textContent = size;
const settings = await getSettings();
settings.bubbleSize = size;
await saveSettings(settings);
await applyTheme(await getTheme());
};
shadowRoot.querySelector('.ulm-position-grid').onclick = async (e) => {
if(e.target.id.startsWith('position-')) {
const [, vertical, horizontal] = e.target.id.split('-');
const newPosition = { vertical, horizontal };
await saveButtonPosition(newPosition);
applyButtonPosition(newPosition);
}
};
// Settings Tab
const settingsToggles = {
'.ulm-toggle-animations': 'animationsEnabled',
'.ulm-toggle-new-tab': 'openInNewTab',
'.ulm-toggle-click-count': 'showClickCount',
'.ulm-toggle-confirm-delete': 'confirmDelete',
'.ulm-toggle-favicons': 'showFavicons'
};
for (const [selector, key] of Object.entries(settingsToggles)) {
shadowRoot.querySelector(selector).onchange = async (e) => {
const settings = await getSettings();
settings[key] = e.target.checked;
await saveSettings(settings);
await refreshUI();
await applyTheme(await getTheme());
};
}
shadowRoot.querySelector('.ulm-recent-count-slider').oninput = async (e) => {
const count = parseInt(e.target.value);
shadowRoot.querySelector('.ulm-recent-count-value').textContent = count;
const settings = await getSettings();
settings.recentCount = count;
await saveSettings(settings);
if (showRecentlyAdded) await refreshUI();
};
shadowRoot.querySelector('.ulm-save-exclude-btn').onclick = async () => {
const input = shadowRoot.querySelector('.ulm-exclude-url-input');
const domain = input.value.trim();
if (!domain) return;
const excluded = await getExcludedDomains();
if (excluded.includes(domain)) { showToast('Domain already excluded!', 'warning'); return; }
excluded.push(domain);
await saveExcludedDomains(excluded);
input.value = '';
populateExcludeList(excluded, shadowRoot.querySelector('.ulm-exclude-list'));
};
shadowRoot.querySelector('.ulm-delete-exclude-btn').onclick = async () => {
isExcludeDeleteMode = !isExcludeDeleteMode;
shadowRoot.querySelector('.ulm-delete-exclude-btn').textContent = isExcludeDeleteMode ? 'Done' : 'Manage Excludes';
populateExcludeList(await getExcludedDomains(), shadowRoot.querySelector('.ulm-exclude-list'));
};
shadowRoot.querySelector('.ulm-hide-btn').onclick = () => {
bubble.classList.add('hidden');
showBubbleButton.classList.add('visible');
mainUI.classList.remove('visible');
bubbleMenu.classList.remove('visible');
saveBubbleHiddenState(true);
};
shadowRoot.querySelector('.ulm-reset-btn').onclick = async () => {
if (confirm('This will reset ALL data. Are you sure?') && confirm('This cannot be undone! Continue?')) {
for (const key of Object.values(STORAGE_KEYS)) { await setData(key, undefined); }
location.reload();
}
};
// Backup Tab
const exportArea = shadowRoot.querySelector('.ulm-export-area');
const copyBtn = shadowRoot.querySelector('.ulm-copy-export-btn');
const showExportData = (data) => {
exportArea.value = JSON.stringify(data, null, 2);
exportArea.style.display = 'block';
copyBtn.style.display = 'block';
};
shadowRoot.querySelector('.ulm-export-all-btn').onclick = async () => {
showExportData({
links: await getLinks(), categories: await getCategories(),
settings: await getSettings(), customColors: await getCustomColors(),
theme: await getTheme(), excludedDomains: await getExcludedDomains(),
clickStats: await getClickStats(), folders: await getFolders()
});
};
shadowRoot.querySelector('.ulm-export-links-only-btn').onclick = async () => showExportData(await getLinks());
copyBtn.onclick = () => { navigator.clipboard.writeText(exportArea.value).then(() => showToast('Copied to clipboard!'), () => showToast('Copy failed!', 'error')); };
shadowRoot.querySelector('.ulm-import-all-btn').onclick = async () => {
const data = shadowRoot.querySelector('.ulm-import-area').value.trim();
if (!data) return;
try {
const parsed = JSON.parse(data);
if (parsed.links) await saveLinks(parsed.links);
if (parsed.categories) await saveCategories(parsed.categories);
if (parsed.settings) await saveSettings(parsed.settings);
if (parsed.customColors) await saveCustomColors(parsed.customColors);
if (parsed.theme) await saveTheme(parsed.theme);
if (parsed.excludedDomains) await saveExcludedDomains(parsed.excludedDomains);
if (parsed.clickStats) await setData(STORAGE_KEYS.clickStats, parsed.clickStats);
if (parsed.folders) await saveFolders(parsed.folders);
showToast('All data imported successfully! Reloading...', 'success');
setTimeout(() => location.reload(), 1000);
} catch (e) { showToast('Invalid data format!', 'error'); }
};
shadowRoot.querySelector('.ulm-import-links-btn').onclick = async () => {
const data = shadowRoot.querySelector('.ulm-import-area').value.trim();
if (!data) return;
try {
const parsed = JSON.parse(data);
const linksToImport = Array.isArray(parsed) ? parsed : parsed.links;
if (!linksToImport) throw new Error('No links found in data.');
await saveLinks(linksToImport);
showToast('Links imported successfully!');
await refreshUI();
} catch (e) { showToast('Invalid data format!', 'error'); }
};
shadowRoot.querySelector('.ulm-clear-stats-btn').onclick = async () => {
if (confirm('Clear all click statistics?')) {
await setData(STORAGE_KEYS.clickStats, {});
showToast('Click stats cleared!');
await refreshUI();
}
};
shadowRoot.querySelector('.ulm-health-check-btn').onclick = async () => {
const links = await getLinks();
if (links.length === 0) { showToast('No links to check.', 'info'); return; }
showToast(`Checking ${links.length} links...`, 'info');
links.forEach(link => link.health.status = 'checking');
await saveLinks(links);
await refreshUI();
const checkPromises = links.map(async (link, index) => {
const result = await checkLinkHealth(link.url);
const allLinks = await getLinks();
allLinks[index].health = {
status: result.ok ? 'ok' : 'error',
checkedAt: Date.now()
};
await saveLinks(allLinks);
});
const interval = setInterval(async () => await refreshUI(), 2000);
await Promise.all(checkPromises);
clearInterval(interval);
showToast('Health check complete!');
await refreshUI();
};
shadowRoot.querySelector('.ulm-add-folder-btn').onclick = async () => {
const input = shadowRoot.querySelector('.ulm-new-folder-input');
const newFolderName = input.value.trim();
if (!newFolderName) return;
const folders = await getFolders();
if (folders.some(f => f.toLowerCase() === newFolderName.toLowerCase())) { showToast('Folder already exists!', 'warning'); return; }
folders.push(newFolderName);
await saveFolders(folders);
input.value = '';
await refreshUI();
};
// Hide menu on outside click
document.addEventListener('click', (e) => {
if (bubbleMenu && !bubble.contains(e.target) && !bubbleMenu.contains(e.target)) {
bubbleMenu.classList.remove('visible');
}
});
}
// --- Instant Add Current Page ---
async function instantAddCurrentPage() {
const url = window.location.href;
if (await checkDuplicate(url)) {
showToast('This page is already in your links!', 'warning');
return;
}
const autoCategory = await suggestCategory(url);
const links = await getLinks();
const title = document.title || 'Untitled';
links.push({ label: title, url, category: autoCategory, addedAt: Date.now(), tags: [], folder: '', health: { status: 'unknown', checkedAt: 0 } });
await saveLinks(links);
bubbleMenu.classList.remove('visible');
showToast(`Added "${title}"!`);
if (mainUI.classList.contains('visible')) {
await refreshUI();
}
}
// --- Context Menu for Links ---
function setupContextMenu() {
const linkList = shadowRoot.querySelector('.ulm-link-list');
const contextMenu = document.createElement('div');
contextMenu.className = 'ulm-context-menu';
shadowRoot.appendChild(contextMenu);
linkList.addEventListener('contextmenu', async (e) => {
const linkWrapper = e.target.closest('.ulm-link-wrapper');
if (!linkWrapper) return;
e.preventDefault();
e.stopPropagation();
const linkIndex = parseInt(linkWrapper.dataset.index);
const links = await getLinks();
const linkData = links[linkIndex];
contextMenu.innerHTML = `
<div class="ulm-ctx-item" data-action="newtab">🆕 Open in New Tab</div>
<div class="ulm-ctx-item" data-action="copy">📋 Copy URL</div>
<div class="ulm-ctx-item" data-action="edit">✏️ Edit</div>
<div class="ulm-ctx-item" data-action="health">🩺 Check Health</div>
<div class="ulm-ctx-item" data-action="delete" style="color:#f44336">🗑️ Delete</div>
`;
contextMenu.style.display = 'block';
contextMenu.style.left = e.clientX + 'px';
contextMenu.style.top = e.clientY + 'px';
contextMenu.onclick = async (ev) => {
const action = ev.target.dataset.action;
if (!action) return;
contextMenu.style.display = 'none';
if (action === 'newtab') window.open(linkData.url, '_blank');
else if (action === 'copy') navigator.clipboard.writeText(linkData.url).then(() => showToast('URL copied!'));
else if (action === 'edit') showEditLinkModal(linkIndex);
else if (action === 'delete') linkWrapper.querySelector('.ulm-delete-btn')?.click();
else if (action === 'health') {
showToast(`Checking "${linkData.label}"...`, 'info');
links[linkIndex].health.status = 'checking';
await saveLinks(links);
await refreshUI();
const result = await checkLinkHealth(linkData.url);
links[linkIndex].health = { status: result.ok ? 'ok' : 'error', checkedAt: Date.now() };
await saveLinks(links);
showToast(result.ok ? 'Link is healthy!' : 'Link seems broken.', result.ok ? 'success' : 'error');
await refreshUI();
}
};
});
shadowRoot.addEventListener('click', () => { contextMenu.style.display = 'none'; });
}
// --- Initialize Script ---
async function initializeScript() {
if (document.getElementById('universal-link-manager-container')) return;
await new Promise(resolve => { if (document.body) resolve(); else window.addEventListener('DOMContentLoaded', resolve); });
shadowRoot = createShadowContainer();
styleElement = document.createElement('style');
shadowRoot.appendChild(styleElement);
bubble = document.createElement('div');
bubble.className = 'ulm-bubble';
shadowRoot.appendChild(bubble);
showBubbleButton = document.createElement('div');
showBubbleButton.className = 'ulm-show-button';
shadowRoot.appendChild(showBubbleButton);
bubbleMenu = document.createElement('div');
bubbleMenu.className = 'ulm-mini-menu';
bubbleMenu.innerHTML = `<button class="ulm-instant-add-btn">⚡ Add This Site</button><button class="ulm-show-menu-btn">⚙️ Open Menu</button>`;
shadowRoot.appendChild(bubbleMenu);
mainUI = document.createElement('div');
mainUI.className = 'ulm-main-ui';
mainUI.innerHTML = createMainUIHTML();
shadowRoot.appendChild(mainUI);
await applyTheme(await getTheme());
applyButtonPosition(await getButtonPosition());
if (await getBubbleHiddenState()) {
bubble.classList.add('hidden');
showBubbleButton.classList.add('visible');
}
setupEventListeners();
setupContextMenu();
await refreshUI();
console.log('Universal Link Manager Pro v5.0 initialized');
}
initializeScript();
})();