Advanced tag organization and folder system for map-making.app
// ==UserScript==
// @name Map-Making Folders
// @icon https://img.icons8.com/?size=200&id=3509&format=png&color=ffffff
// @namespace https://map-making.app/
// @version 7.6.2
// @description Advanced tag organization and folder system for map-making.app
// @author Youri Auski
// @match https://map-making.app/*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const getMapId = () => { const m = location.pathname.match(/\/maps\/([^/?#]+)/); return m ? m[1] : null; };
const SK = () => `mmapp_v6_${getMapId() || 'global'}`;
let S = { folders: [], lastUpdate: 0 };
let isDirty = false;
const setDirty = (state) => {
isDirty = state;
const btn = document.getElementById('mm-folder-save-btn');
if (btn) {
if (state) {
btn.classList.add('mm-btn-dirty');
btn.innerHTML = '<span class="button__label">Save Folders</span>';
} else {
btn.classList.remove('mm-btn-dirty');
btn.innerHTML = '<span class="button__label">Folders Saved</span>';
}
}
};
const loadS = () => {
try {
const r = localStorage.getItem(SK());
if (r) S = { folders: [], lastUpdate: 0, ...JSON.parse(r) };
setDirty(false);
} catch (_) {}
};
const saveS = (markDirty = true) => {
try {
invalidateFlatCache();
S.lastUpdate = Date.now();
localStorage.setItem(SK(), JSON.stringify(S));
if (markDirty) {
setDirty(true);
}
} catch (_) {}
};
const waitDOM = (selector, root = document, timeout = 3000) => {
return new Promise(resolve => {
const existing = root.querySelector(selector);
if (existing) return resolve(existing);
const obs = new MutationObserver(() => {
const el = root.querySelector(selector);
if (el) { obs.disconnect(); resolve(el); }
});
obs.observe(root, { childList: true, subtree: true });
setTimeout(() => { obs.disconnect(); resolve(null); }, timeout);
});
};
const uid = () => Math.random().toString(36).slice(2, 8) + Math.random().toString(36).slice(2, 8);
const hexToRgb = h => {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h);
return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [100, 100, 200];
};
const lerp = (h1, h2, t) => {
const a = hexToRgb(h1), b = hexToRgb(h2);
return `rgb(${Math.round(a[0] + (b[0] - a[0]) * t)},${Math.round(a[1] + (b[1] - a[1]) * t)},${Math.round(a[2] + (b[2] - a[2]) * t)})`;
};
const lum = c => {
const v = (c.startsWith('#') ? hexToRgb(c) : c.match(/\d+/g)?.map(Number)) || [128, 128, 128];
return (0.299 * v[0] + 0.587 * v[1] + 0.114 * v[2]) / 255;
};
const textOn = bg => lum(bg) > 0.45 ? '#111' : '#fff';
const rgbToHex = rgb => {
if (rgb.startsWith('#')) return rgb;
const m = rgb.match(/\d+/g);
if (!m || m.length < 3) return '#ffffff';
return '#' + m.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
};
let _flatCache = null;
let _flatCacheDirty = true;
let _allTagsCache = null;
const invalidateFlatCache = () => {
_flatCacheDirty = true;
_allTagsCache = null;
};
const _flatRaw = (l = S.folders) => l.flatMap(f => [f, ..._flatRaw(f.children || [])]);
const flat = () => {
if (_flatCacheDirty || !_flatCache) {
_flatCache = _flatRaw(S.folders);
_flatCacheDirty = false;
}
return _flatCache;
};
const getAssignedTags = () => {
if (!_allTagsCache) _allTagsCache = new Set(flat().flatMap(f => f.tags || []));
return _allTagsCache;
};
let recentPresets = [];
try { recentPresets = JSON.parse(localStorage.getItem('mmapp_recent_presets') || '[]'); } catch (_) {}
const saveRecentPreset = p => {
recentPresets = recentPresets.filter(r => r.name !== p.name);
recentPresets.unshift(p);
if (recentPresets.length > 4) recentPresets.length = 4;
localStorage.setItem('mmapp_recent_presets', JSON.stringify(recentPresets));
};
const GRADIENT_PRESETS = [
// =====================================================================
// REDS & FLAMES / ROUGES & FLAMMES (25)
// =====================================================================
{ name: 'Volcanic Rouge Red', colors: ['#1a0000', '#8b1a00', '#ff4500', '#ffaa00'] },
{ name: 'Ember Rouge Red', colors: ['#2d0000', '#c0392b', '#e74c3c', '#f39c12'] },
{ name: 'Magma Flow Rouge Red', colors: ['#0d0000', '#3d0000', '#9b2335', '#ff6b35', '#ffd700'] },
{ name: 'Inferno Rouge Red', colors: ['#120000', '#4a0000', '#aa1100', '#ff4400', '#ffaa00', '#ffff00'] },
{ name: 'Phoenix Rouge Red', colors: ['#1a0000', '#7d0000', '#c0392b', '#e67e22', '#f1c40f'] },
{ name: 'Campfire Rouge Red', colors: ['#1a0a00', '#7a2800', '#e85d04', '#f4a261', '#ffcb77'] },
{ name: 'Red Giant Rouge Red', colors: ['#100000', '#400000', '#a00000', '#ff4400', '#ffaa00'] },
{ name: 'Sunset Blaze Rouge Red', colors: ['#1a0028', '#8b0050', '#cc2200', '#ff7700', '#ffdd00'] },
{ name: 'Hot Coals Rouge Red', colors: ['#1c0a00', '#5c1a00', '#cc3300', '#ff7700'] },
{ name: 'Cinnabar Glow Rouge Red', colors: ['#6a0010', '#e03020', '#f89060'] },
{ name: 'Dark Rust Rouge Red', colors: ['#1a0800', '#600800', '#c02800'] },
{ name: 'Kiln Rouge Red', colors: ['#200000', '#602000', '#d45500', '#f08020'] },
{ name: 'Crimson Tide Rouge Red', colors: ['#180008', '#660020', '#cc0030', '#ff4060'] },
{ name: 'Forge Rouge Red', colors: ['#0d0000', '#3a0a00', '#aa4400', '#dd7722', '#ffaa44'] },
{ name: 'Lava Lamp Rouge Red', colors: ['#1a0028', '#ff0066', '#ff6600', '#ffcc00'] },
{ name: 'Ruby Depths Rouge Red', colors: ['#0a0005', '#400010', '#900030', '#d00050', '#ff6080'] },
{ name: 'Pomegranate Rouge Red', colors: ['#3a0010', '#880030', '#cc2040', '#e87880'] },
{ name: 'Blood Moon Rouge Red', colors: ['#0a0000', '#3a0800', '#8a1a10', '#c84030', '#e88060'] },
{ name: 'Chili Rouge Red', colors: ['#2a0000', '#7a1000', '#c03000', '#e06020', '#f0a060'] },
{ name: 'Molten Rouge Red', colors: ['#0a0000', '#4a0800', '#c03000', '#f06000', '#ffb030'] },
{ name: 'Garnet Rouge Red', colors: ['#300010', '#800030', '#c00050', '#e04080', '#f890b0'] },
{ name: 'Carnelian Rouge Red', colors: ['#3a0808', '#9a2808', '#d05020', '#e88050', '#f8c0a0'] },
{ name: 'Rage Rouge Red', colors: ['#1a0000', '#6a0000', '#cc0000', '#ff4000'] },
{ name: 'Scarlet Smoke Rouge Red', colors: ['#2a0808', '#802020', '#c05040', '#e09080'] },
{ name: 'Fire & Ice Rouge Red Bleu Blue', colors: ['#cc0000', '#ff6600', '#ffffff', '#0066ff', '#0000cc'] },
// =====================================================================
// GREENS / VERTS (15)
// =====================================================================
{ name: 'Jungle Canopy Vert Green', colors: ['#0a1a00', '#1a4000', '#2d7020', '#60a840', '#a0d060'] },
{ name: 'Emerald Night Vert Green', colors: ['#020a04', '#0a5828', '#30c878'] },
{ name: 'Spring Meadow Vert Green', colors: ['#102800', '#2a6010', '#50a030', '#90d060', '#d0f890'] },
{ name: 'Fern Hollow Vert Green', colors: ['#0a2010', '#1a5028', '#30a050', '#70d080', '#c0f0a0'] },
{ name: 'Pine Resin Vert Green', colors: ['#0a1a08', '#1a3808', '#307020', '#60b040', '#a0d880'] },
{ name: 'Malachite Vert Green', colors: ['#001a08', '#006030', '#00c860', '#40e888', '#a0ffc0'] },
{ name: 'Radioactive Vert Green', colors: ['#001000', '#004000', '#00a000', '#80ff00', '#ccff80'] },
{ name: 'Acid Rain Vert Green', colors: ['#001008', '#004020', '#00c060', '#80ff40', '#ccff00'] },
{ name: 'Cyber Green Vert Green', colors: ['#001008', '#002a18', '#007040', '#00cc80', '#00ff88'] },
{ name: 'Circuit Vert Green', colors: ['#001808', '#003818', '#006030', '#00a050', '#00e880'] },
{ name: 'Absinthe Vert Green', colors: ['#0a1800', '#2a5000', '#60a000', '#a0d020', '#d0f060'] },
{ name: 'Sage Meadow Vert Green', colors: ['#1a2808', '#3a5820', '#6a9040', '#9ab860', '#c8e090'] },
{ name: 'Dark Fern Vert Green', colors: ['#0a180a', '#207020', '#50b050'] },
{ name: 'Forest Amber Vert Green Jaune Yellow', colors: ['#1a2008', '#385820', '#a89020'] },
// =====================================================================
// BLUES / BLEUS (10)
// =====================================================================
{ name: 'Deep Dive Bleu Blue', colors: ['#000510', '#001a40', '#003070', '#0060aa', '#00aacc'] },
{ name: 'Pacific Bleu Blue', colors: ['#001020', '#003050', '#006688', '#00aabb', '#00ddee'] },
{ name: 'Wave Break Bleu Blue', colors: ['#002040', '#004080', '#0080c0', '#40c0e0', '#c0f0ff'] },
{ name: 'Electric Blue Bleu Blue', colors: ['#000520', '#000880', '#0040ff', '#00ccff', '#80ffff'] },
{ name: 'Cobalt Night Bleu Blue', colors: ['#030510', '#081888', '#4070e8'] },
{ name: 'Lapis Bleu Blue', colors: ['#000830', '#001870', '#0040c0', '#3070e0', '#7090ff'] },
{ name: 'Nordic Bleu Blue', colors: ['#0a1820', '#183060', '#306098', '#5898d8', '#90c8f0'] },
// =====================================================================
// YELLOWS & GOLDS / JAUNES & ORS (15)
// =====================================================================
{ name: 'Gold Jaune Yellow', colors: ['#2a1800', '#8a5800', '#d0a000', '#f0d040', '#fff8c0'] },
{ name: 'Saffron Jaune Yellow', colors: ['#3a1000', '#aa4000', '#e08000', '#f8c000', '#fff080'] },
{ name: 'Honey Jaune Yellow', colors: ['#3a1800', '#9a5000', '#e09020', '#f8c840', '#fff8a0'] },
{ name: 'Summer Noon Jaune Yellow', colors: ['#fff9c4', '#fff176', '#ffee58', '#fdd835', '#f9a825'] },
{ name: 'Sulfur Jaune Yellow', colors: ['#1a1800', '#5a5000', '#c0a800', '#f0d800', '#ffffa0'] },
{ name: 'Pyrite Jaune Yellow', colors: ['#2a2000', '#6a5010', '#c0a020', '#e8c840', '#f8f080'] },
{ name: 'Butterscotch Jaune Yellow', colors: ['#3a1808', '#a05818', '#d09038', '#f0c860', '#fff8c0'] },
{ name: 'Caramel Jaune Yellow Marron Brown', colors: ['#2a1000', '#8a4010', '#d08030', '#e8c070', '#f8f0c0'] },
{ name: 'Sunbaked Jaune Yellow', colors: ['#4a2800', '#9a5010', '#d08030', '#f0b060', '#f8e0a0'] },
{ name: 'Deco Gold Jaune Yellow', colors: ['#1a1200', '#5a4200', '#c09000', '#e8c840', '#f8f0c0'] },
{ name: 'Harvest Moon Jaune Yellow', colors: ['#1a0800', '#7a3000', '#c07020', '#e0a840', '#f8d880'] },
{ name: 'Topaz Jaune Yellow', colors: ['#201000', '#703000', '#d08000', '#f0c040', '#fff0a0'] },
{ name: 'Smoked Gold Jaune Yellow', colors: ['#3a3020', '#9a8830', '#e8d060'] },
{ name: 'Brass Jaune Yellow Marron Brown', colors: ['#a08020', '#c8a840', '#e8c870'] },
{ name: 'Solar Flare Jaune Yellow Rouge Red', colors: ['#ff0000', '#ff6600', '#ffcc00'] },
// =====================================================================
// PURPLES & VIOLETS / VIOLETS (15)
// =====================================================================
{ name: 'Amethyst Violet Purple', colors: ['#1a0028', '#4a0080', '#8800cc', '#cc66ff', '#f0ccff'] },
{ name: 'Nebula Violet Purple', colors: ['#0a0020', '#300060', '#6000c0', '#c040e0', '#ff80ff'] },
{ name: 'Vaporwave Violet Purple', colors: ['#1a0040', '#6600cc', '#cc00aa', '#ff0066', '#ff6600'] },
{ name: 'Retrowave Violet Purple', colors: ['#080018', '#200050', '#800080', '#ff00ff', '#ff8800'] },
{ name: 'Quasar Violet Purple', colors: ['#080010', '#280040', '#7800c0', '#c040ff', '#ff80dd'] },
{ name: 'Andromeda Violet Purple', colors: ['#080018', '#180040', '#400088', '#8800cc', '#dd66ff'] },
{ name: 'Synthwave Violet Purple', colors: ['#0a0020', '#2a0060', '#cc00ff', '#ff0099', '#ff6600'] },
{ name: 'Plasma Violet Purple', colors: ['#080018', '#4000a0', '#c000ff', '#ff00cc', '#ffaa00'] },
{ name: 'Orchid Dusk Violet Purple', colors: ['#1a1028', '#8838b8', '#e888e0'] },
{ name: 'Deep Wisteria Violet Purple', colors: ['#1a0838', '#7850b0', '#d0b0e8'] },
{ name: 'Night Bloom Violet Purple', colors: ['#0a0818', '#380858', '#a840c8', '#f090f0'] },
{ name: 'Berry Smoke Violet Purple', colors: ['#280830', '#8a2870', '#e060c0'] },
{ name: 'Dusk Periwinkle Violet Purple', colors: ['#2a2848', '#8088d0', '#d0d8f8'] },
{ name: 'Plum Amber Violet Purple Jaune Yellow', colors: ['#2a0830', '#7a2860', '#e09030'] },
{ name: 'Ink Bloom Violet Purple', colors: ['#0a0818', '#4a2060', '#c870d0'] },
// =====================================================================
// PINKS & ROSES / ROSES (10)
// =====================================================================
{ name: 'Cherry Blossom Rose Pink', colors: ['#fce4ec', '#f8bbd0', '#f48fb1', '#f06292', '#ec407a'] },
{ name: 'Cotton Candy Rose Pink', colors: ['#ffb3c6', '#ffc6e0', '#e8c0f8', '#c0d0ff', '#b8f0ff'] },
{ name: 'Sakura Rose Pink', colors: ['#fae0e8', '#f8c8d8', '#f0a8c0', '#e888a8', '#e07090'] },
{ name: 'Hot Pink Rose Pink', colors: ['#200010', '#800050', '#ff0080', '#ff88cc', '#ffc0ff'] },
{ name: 'Raspberry Rose Pink', colors: ['#2a0018', '#880040', '#d80068', '#f840a0', '#ff90cc'] },
{ name: 'Bubblegum Rose Pink', colors: ['#ff99bb', '#ff99ee', '#cc99ff', '#99bbff', '#99ffee'] },
{ name: 'Hibiscus Rose Pink', colors: ['#3a0018', '#a01840', '#e84880', '#f890b8', '#ffd0e0'] },
{ name: 'Rose Quartz Rose Pink', colors: ['#3a1428', '#a04870', '#e080a8', '#f8c0d0', '#fff0f5'] },
{ name: 'Dreamsicle Rose Pink Orange', colors: ['#ffe0c0', '#ffcc99', '#ffaa88', '#ff8899', '#ff66bb'] },
{ name: 'Blush Marble Rose Pink', colors: ['#e8c8c8', '#f8e8e0', '#fff8f5'] },
// =====================================================================
// ORANGES (10)
// =====================================================================
{ name: 'Terracotta Orange', colors: ['#7a2810', '#c05030', '#e08050', '#f0b090', '#f8d8c0'] },
{ name: 'Mesa Orange', colors: ['#2a1000', '#7a3810', '#c07030', '#e0b060', '#f0d8a0'] },
{ name: 'Indian Summer Orange', colors: ['#2a1000', '#8a4000', '#e07818', '#f8b840', '#fff8c0'] },
{ name: 'Jasper Orange', colors: ['#8a3818', '#c86830', '#f0b888'] },
{ name: 'Tangerine Smoke Orange', colors: ['#2a1a10', '#e08820'] },
{ name: 'Mango Orange', colors: ['#3a1800', '#c06010', '#f09030', '#f8c860', '#fff0b0'] },
{ name: 'Rust Belt Orange', colors: ['#2a1200', '#8b3a00', '#c46a1a', '#e8a030'] },
{ name: 'Bronze Orange Marron Brown', colors: ['#1a0800', '#6a3000', '#b07020', '#e0b050', '#f8e090'] },
// =====================================================================
// TEALS & CYANS / CYANS (12)
// =====================================================================
{ name: 'Aurora Borealis Cyan', colors: ['#000a20', '#003030', '#00aa88', '#00ff80', '#88ffcc'] },
{ name: 'Bioluminescent Cyan', colors: ['#000820', '#001540', '#003366', '#00aaff', '#00ffcc'] },
{ name: 'Tide Pool Cyan', colors: ['#0a1a2a', '#1a4060', '#2a8080', '#40c0b0', '#a0f0e0'] },
{ name: 'Blue Lagoon Cyan', colors: ['#001a30', '#007070', '#00c0a0', '#80f0e0'] },
{ name: 'Sea Glass Cyan', colors: ['#2a5050', '#50a090', '#80d0c0', '#c0f0e8'] },
{ name: 'Tidal Surge Cyan', colors: ['#000810', '#002050', '#0060b0', '#20b0f0', '#a0ffff'] },
{ name: 'Peacock Ore Cyan', colors: ['#001010', '#003838', '#006868', '#40a8c0', '#80e8ff'] },
{ name: 'Ion Storm Cyan', colors: ['#000820', '#002060', '#0080ff', '#00ffcc', '#80ffff'] },
{ name: 'Tidal Pool Cyan', colors: ['#0a2020', '#2a7878', '#80d8d0', '#d0f8f8'] },
{ name: 'Tourmaline Cyan Vert Green', colors: ['#001820', '#005840', '#00a860', '#60e880', '#ccffcc'] },
{ name: 'Laser Grid Cyan', colors: ['#000810', '#001840', '#00a0ff', '#00ffaa'] },
// =====================================================================
// BROWNS & EARTH TONES / MARRONS & TERRES (13)
// =====================================================================
{ name: 'Espresso Marron Brown', colors: ['#0a0500', '#2a1008', '#5a2810', '#8a5020', '#c09040'] },
{ name: 'Mocha Marron Brown', colors: ['#1a0808', '#4a2010', '#8a4820', '#c08050', '#e8b890'] },
{ name: 'Sepia Marron Brown', colors: ['#1a1000', '#5a4010', '#a08030', '#d0b870', '#f0e8c0'] },
{ name: 'Canyon Marron Brown', colors: ['#3a1000', '#8a3010', '#c06020', '#e0a060', '#f0d0a0'] },
{ name: 'Redwood Marron Brown', colors: ['#1a0800', '#5a1800', '#a04020', '#c07848', '#d8b090'] },
{ name: 'Ochre Marron Brown Jaune Yellow', colors: ['#3a2800', '#8a6010', '#d0a020', '#f0d050', '#f8f0c0'] },
{ name: 'Adobe Marron Brown', colors: ['#6a3018', '#b07050', '#d0a880', '#e8d0b8'] },
{ name: 'Walnut Sage Marron Brown Vert Green', colors: ['#3a2010', '#6a4a20', '#90a870'] },
{ name: 'Cognac Marron Brown', colors: ['#3a1000', '#8a3a10', '#d07030', '#f0c080'] },
{ name: 'Mahogany Marron Brown', colors: ['#2a0808', '#6a1818', '#c06848', '#e8c0a0'] },
{ name: 'Tiger Eye Marron Brown Jaune Yellow', colors: ['#3a1800', '#8a4800', '#d08020', '#e8b860', '#f8e0a0'] },
{ name: 'Smoked Amber Marron Brown Jaune Yellow', colors: ['#3a3020', '#b09030', '#e8d060'] },
{ name: 'Western Marron Brown', colors: ['#3a1a08', '#8a5020', '#c08840', '#e0b870', '#f0ddb0'] },
// =====================================================================
// MIXED DUOS / MÉLANGES INÉDITS
// =====================================================================
{ name: 'Copper Teal Marron Brown Cyan', colors: ['#5a2800', '#a06030', '#408080', '#40c0c0'] },
{ name: 'Mocha Blue Marron Brown Bleu Blue', colors: ['#3a1808', '#8a5030', '#2050a0', '#4090e0'] },
{ name: 'Earth Sky Marron Brown Bleu Blue', colors: ['#6a3810', '#c09040', '#408888', '#80c8e0'] },
{ name: 'Bark Violet Marron Brown Violet Purple', colors: ['#5a3020', '#a07050', '#7040a0', '#c080e0'] },
{ name: 'Rust Cyan Orange Cyan', colors: ['#9a2800', '#e06020', '#20a0a0', '#60f0e0'] },
{ name: 'Coffee Mint Marron Brown Vert Green', colors: ['#3a1808', '#8a5020', '#20a060', '#80e0b0'] },
{ name: 'Umber Teal Marron Brown Cyan', colors: ['#4a2808', '#9a6030', '#1a7878', '#40c0b8'] },
{ name: 'Bronze Cerulean Marron Brown Bleu Blue', colors: ['#6a3000', '#b07020', '#2060c0', '#60a0ff'] },
{ name: 'Caramel Azure Jaune Yellow Bleu Blue', colors: ['#c07820', '#e0b040', '#2080c0', '#80c8ff'] },
{ name: 'Clay Indigo Marron Brown Violet Purple', colors: ['#8a4020', '#d08050', '#2020a0', '#6060e0'] },
{ name: 'Coral Turquoise Rose Pink Cyan', colors: ['#e05050', '#f09080', '#20b0b0', '#60f0e0'] },
{ name: 'Gold Violet Jaune Yellow Violet Purple', colors: ['#c09000', '#e8c030', '#6020a0', '#c060e0'] },
{ name: 'Jade Amber Vert Green Jaune Yellow', colors: ['#1a7850', '#40c888', '#c89020', '#f0d040'] },
{ name: 'Navy Saffron Bleu Blue Jaune Yellow', colors: ['#0a1848', '#2858c8', '#e0a000', '#f8d840'] },
{ name: 'Magenta Lime Rose Pink Vert Green', colors: ['#c00060', '#f040a0', '#60c000', '#b0f000'] },
{ name: 'Aqua Peach Cyan Orange', colors: ['#0a9090', '#40e0d0', '#e08050', '#f8c090'] },
{ name: 'Midnight Gold Noir Black Jaune Yellow', colors: ['#0a0808', '#1a1018', '#c08000', '#f0d040'] },
{ name: 'Green Burgundy Vert Green Rouge Red', colors: ['#1a4820', '#40a850', '#7a0820', '#c04060'] },
{ name: 'Arctic Ember Bleu Blue Rouge Red', colors: ['#c8e8f8', '#80c0e8', '#e85020', '#ff8040'] },
{ name: 'Forest Crimson Vert Green Rouge Red', colors: ['#0a2808', '#308030', '#880020', '#d04060'] },
{ name: 'Slate Lime Gris Gray Vert Green', colors: ['#3a4050', '#606880', '#a0c820', '#d0f050'] },
{ name: 'Charcoal Teal Gris Gray Cyan', colors: ['#1a2020', '#404848', '#10a090', '#40e0d0'] },
{ name: 'Plum Gold Violet Purple Jaune Yellow', colors: ['#2a0840', '#7820a0', '#c09000', '#f0c830'] },
{ name: 'Smoke Coral Gris Gray Orange', colors: ['#504850', '#909090', '#e07050', '#f8b090'] },
{ name: 'Ice Magma Bleu Blue Rouge Red', colors: ['#a0d0f8', '#4090e8', '#0020a0', '#d04000', '#ff8000'] },
// =====================================================================
// GALAXIES & COSMOS (10)
// =====================================================================
{ name: 'Pulsar Violet Purple Bleu Blue', colors: ['#000820', '#001840', '#0040a0', '#00aaff', '#80ffff'] },
{ name: 'Stardust Violet Purple', colors: ['#080010', '#200030', '#580060', '#9040a0', '#e0a0e0'] },
{ name: 'Binary Star Bleu Blue Jaune Yellow', colors: ['#000820', '#00204a', '#0080c0', '#ff8800', '#ffdd00'] },
{ name: 'Event Horizon Violet Purple', colors: ['#000000', '#100020', '#300060', '#700090', '#aa00cc'] },
{ name: 'Dark Matter Violet Purple', colors: ['#050005', '#100020', '#200040', '#400080'] },
{ name: 'Solar Wind Violet Purple', colors: ['#0a0818', '#2a1040', '#6040c0', '#c0a0ff', '#ffd0ff'] },
{ name: 'Cosmic Dust Violet Purple', colors: ['#0a0820', '#180840', '#380868', '#680890', '#a838b8'] },
{ name: 'Zodiac Violet Purple Rouge Red', colors: ['#0a0010', '#200040', '#4000a0', '#9000d0', '#f000f0', '#ff6600'] },
{ name: 'Supernova Bleu Blue', colors: ['#000820', '#002040', '#0060a0', '#40b0ff', '#ffffff'] },
{ name: 'Wormhole Violet Purple', colors: ['#000000', '#1a0030', '#5000a0', '#b040ff', '#ffffff'] },
// =====================================================================
// NEON & CYBERPUNK (8)
// =====================================================================
{ name: 'Neon City Violet Purple Rouge Red', colors: ['#080010', '#2000a0', '#8000ff', '#ff00aa', '#ff6600'] },
{ name: 'Hologram Bleu Blue Violet Purple', colors: ['#001840', '#00a0ff', '#00ffee', '#ff00ee', '#ff8800'] },
{ name: 'Glitch Vert Green Violet Purple', colors: ['#000808', '#003000', '#00ff00', '#ff00ff', '#00ffff'] },
{ name: 'UV Violet Purple', colors: ['#0a0020', '#3000a0', '#8000ff', '#cc80ff', '#ffffff'] },
{ name: 'Laser Cut Bleu Blue Cyan', colors: ['#000010', '#0000a0', '#0080ff', '#00ffcc', '#ffffff'] },
{ name: 'Power Grid Bleu Blue Cyan', colors: ['#000008', '#000040', '#001080', '#00a0ff', '#ccffff'] },
{ name: 'Night Market Violet Purple Rose Pink', colors: ['#080818', '#201040', '#602080', '#c060c0', '#f0a0e0'] },
{ name: 'Phase Shift Violet Purple Cyan', colors: ['#0a0028', '#4000c0', '#cc00ff', '#00ccff', '#ccffff'] },
// =====================================================================
// PASTELS & SOFT / PASTELS & DOUX (7)
// =====================================================================
{ name: 'Macaron Rose Pink Vert Green', colors: ['#f9d5e5', '#fce4c5', '#d4f5e9', '#cfe4f7', '#e9d5f0'] },
{ name: 'Baby Blues Bleu Blue', colors: ['#e0eeff', '#c8d8ff', '#a8c0ff', '#88a8ff', '#6888ff'] },
{ name: 'Sherbet Jaune Yellow Rose Pink', colors: ['#fff0c0', '#ffe0a0', '#ffc0a0', '#ffa0c0', '#e0a0e0'] },
{ name: 'Cloud Drift Bleu Blue', colors: ['#f0f8ff', '#e0f0ff', '#d0e8ff', '#c0d8f8', '#b0c8f0'] },
{ name: 'Spearmint Cream Vert Green', colors: ['#e0fff0', '#d0f8e8', '#c0f0e0', '#b0e8d8', '#a0d8d0'] },
{ name: 'Lilac Mist Violet Purple', colors: ['#ece0ff', '#f0d8ff', '#f8d0ff', '#ffd0f0', '#ffe0e8'] },
{ name: 'Morning Mist Vert Green Bleu Blue', colors: ['#f8f0ff', '#e8e8ff', '#d8f0ff', '#c8f8f0', '#d0ffd8'] },
// =====================================================================
// EARTH & DESERTS / TERRES & DÉSERTS (7)
// =====================================================================
{ name: 'Sahara Marron Brown Jaune Yellow', colors: ['#2a1800', '#8a5010', '#d09030', '#f0d070', '#f8f0d0'] },
{ name: 'Red Rock Rouge Red Marron Brown', colors: ['#2a0a00', '#6a2000', '#b05020', '#d09060', '#e8c8a0'] },
{ name: 'Dust Storm Marron Brown', colors: ['#4a3828', '#9a8060', '#d0b888', '#f0e0c8'] },
{ name: 'Gobi Jaune Yellow Marron Brown', colors: ['#3a3020', '#7a7040', '#b8b070', '#e0d8a0', '#f8f5e0'] },
{ name: 'Flint Gris Gray', colors: ['#2a2a28', '#585850', '#888880', '#b8b8b0', '#e0e0d8'] },
{ name: 'Petrified Marron Brown', colors: ['#3a3028', '#7a6850', '#b0a080', '#d8c8a8', '#f0e8d0'] },
{ name: 'Outback Orange Marron Brown', colors: ['#3a1800', '#9a5010', '#d09030', '#e8c070', '#f8e8c0'] },
// =====================================================================
// JEWELS & MINERALS / JOYAUX & MINÉRAUX (19)
// =====================================================================
{ name: 'Sapphire Bleu Blue', colors: ['#000820', '#001068', '#0040c0', '#4080ff', '#80b8ff'] },
{ name: 'Emerald Vert Green', colors: ['#000e08', '#003020', '#007040', '#00c070', '#60e8a0'] },
{ name: 'Ruby Rouge Red', colors: ['#180000', '#600010', '#cc0030', '#ff4060', '#ff99aa'] },
{ name: 'Opal Multicolore Multicolor', colors: ['#f0f8ff', '#d0e8ff', '#e0d0ff', '#ffd0e8', '#d0ffe8'] },
{ name: 'Onyx Noir Black', colors: ['#000000', '#0a0808', '#151010', '#201818', '#2a2020'] },
{ name: 'Alexandrite Violet Purple Vert Green', colors: ['#1a0028', '#6000a0', '#c040e0', '#40d0a0', '#00f0a0'] },
{ name: 'Rhodonite Rose Pink', colors: ['#2a1020', '#7a3050', '#c05880', '#e898b8', '#f8d0e0'] },
{ name: 'Jade Vert Green', colors: ['#0a1a10', '#1a5030', '#40a060', '#80cc90', '#c0f0c0'] },
{ name: 'Labradorite Bleu Blue Gris Gray', colors: ['#181820', '#303848', '#506888', '#7098c0', '#a0c8e8'] },
{ name: 'Selenite Blanc White', colors: ['#d8d0e8', '#e0d8f0', '#e8e0f8', '#f0e8ff', '#f8f4ff'] },
{ name: 'Azurite Bleu Blue', colors: ['#000a20', '#001848', '#003090', '#2060d0', '#60a0ff'] },
{ name: 'Sodalite Bleu Blue Violet Purple', colors: ['#050820', '#101840', '#201870', '#3030a8', '#5858e0'] },
{ name: 'Agate Rose Pink Gris Gray', colors: ['#3a1828', '#8a3858', '#c07090', '#e8a8c0', '#f8d8e8'] },
{ name: 'Carnelian Orange Rouge Red', colors: ['#3a0808', '#9a2808', '#d05020', '#e88050', '#f8c0a0'] },
{ name: 'Cinnabar Rouge Red', colors: ['#2a0800', '#8a1800', '#cc3000', '#f06040', '#f8b0a0'] },
// =====================================================================
// SEASONS & NATURE / SAISONS & NATURE (10)
// =====================================================================
{ name: 'Autumn Canopy Orange', colors: ['#1a0800', '#6a2000', '#c06010', '#e0a020', '#f0d060'] },
{ name: 'Winter Frost Bleu Blue', colors: ['#e8f4ff', '#d0e8ff', '#b8d8ff', '#a0c8ff', '#88b8ff'] },
{ name: 'Spring Thaw Vert Green', colors: ['#d0e8d0', '#b0d8b0', '#80c880', '#60b880', '#40a870'] },
{ name: 'Late Summer Jaune Yellow Rouge Red', colors: ['#f8e070', '#f0b040', '#e88020', '#e06040', '#c84060'] },
{ name: 'Golden Hour Jaune Yellow Orange', colors: ['#1a0800', '#8a3000', '#e07010', '#f8a030', '#fff0a0'] },
{ name: 'Blue Hour Bleu Blue Violet Purple', colors: ['#0a0820', '#181840', '#302878', '#5050a8', '#8080d0'] },
{ name: 'May Day Vert Green Jaune Yellow', colors: ['#a0e860', '#c8f880', '#f0ff80', '#f8e860', '#f8c840'] },
{ name: 'Deep Winter Bleu Blue', colors: ['#0a1020', '#182040', '#283060', '#404880', '#6068a0'] },
{ name: 'Birch Grove Marron Brown Vert Green', colors: ['#c8c0a0', '#e0d8b8', '#f0f0e0', '#d0e8c8', '#a0c888'] },
{ name: 'Rainforest Mist Vert Green', colors: ['#0a1808', '#206030', '#50a060', '#a0d898', '#d0f8c8'] },
// =====================================================================
// METALLICS / MÉTALLIQUES (13)
// =====================================================================
{ name: 'Rose Gold Rose Pink Marron Brown', colors: ['#2a0810', '#8a2840', '#d07070', '#e8a898', '#f8d8d8'] },
{ name: 'Gunmetal Gris Gray Bleu Blue', colors: ['#101518', '#202830', '#304050', '#406070', '#507888'] },
{ name: 'Iridescent Multicolore Multicolor', colors: ['#0040a0', '#00a080', '#80ff80', '#ffd040', '#ff0080'] },
{ name: 'Patina Vert Green Marron Brown', colors: ['#2a4028', '#508060', '#80b890', '#b0d8b8', '#d8f0d8'] },
{ name: 'Burnished Jaune Yellow Marron Brown', colors: ['#2a1800', '#8a5000', '#d09820', '#f0cc60', '#fff8c0'] },
{ name: 'Bismuth Violet Purple Gris Gray', colors: ['#3a3038', '#8070a0', '#c0a8e0', '#f0d8ff', '#ffffff'] },
{ name: 'Pewter Gris Gray', colors: ['#303838', '#506068', '#708888', '#98b0b0', '#c0d0d0'] },
{ name: 'Chrome Gris Gray', colors: ['#202020', '#505050', '#909090', '#c8c8c8', '#ffffff'] },
{ name: 'Anodized Bleu Blue', colors: ['#000838', '#0010a0', '#0090ff', '#80d0ff', '#c0f0ff'] },
{ name: 'Gilded Jaune Yellow Noir Black', colors: ['#080400', '#3a2000', '#9a6000', '#e0a820', '#f8f080'] },
{ name: 'Platinum Gris Gray', colors: ['#484848', '#787878', '#a8a8a8', '#d0d0d0', '#f0f0f0'] },
{ name: 'Silver Gris Gray', colors: ['#383838', '#686868', '#989898', '#c8c8c8', '#f0f0f0'] },
{ name: 'Copper Orange Marron Brown', colors: ['#2a0a00', '#8a3800', '#d07030', '#e8a860', '#f8d0a0'] },
// =====================================================================
// MOOD & EMOTION / HUMEUR & ÉMOTION (10)
// =====================================================================
{ name: 'Serenity Bleu Blue', colors: ['#e0f0ff', '#c0d8f8', '#a0c0f0', '#80a8e0', '#6090d0'] },
{ name: 'Nostalgia Marron Brown', colors: ['#a08060', '#c0a878', '#d8c898', '#f0e8c0', '#f8f0d8'] },
{ name: 'Melancholy Bleu Blue Gris Gray', colors: ['#101828', '#283848', '#405870', '#608898', '#90b8c0'] },
{ name: 'Euphoria Violet Purple', colors: ['#1a0040', '#6000c0', '#cc00ff', '#ff66ff', '#ffccff'] },
{ name: 'Wonder Bleu Blue', colors: ['#000828', '#002060', '#0060d0', '#40a8ff', '#c0e8ff'] },
{ name: 'Triumph Rouge Red Jaune Yellow', colors: ['#200000', '#800000', '#ff0000', '#ff8800', '#ffff00'] },
{ name: 'Passion Rouge Red', colors: ['#1a0000', '#6a0010', '#cc0030', '#ff3060', '#ff80a0'] },
{ name: 'Mystery Violet Purple Noir Black', colors: ['#050010', '#150030', '#350060', '#650090', '#9500c0'] },
{ name: 'Joy Multicolore Multicolor', colors: ['#ff8800', '#ffcc00', '#88ff00', '#00ffcc', '#0088ff'] },
{ name: 'Hope Vert Green Bleu Blue', colors: ['#c0d8f8', '#a0c0f0', '#80e8c8', '#a0f8a0', '#d0f8d0'] },
// =====================================================================
// VINTAGE & RETRO (9)
// =====================================================================
{ name: '70s Harvest Orange', colors: ['#5a2800', '#c86810', '#e0a030', '#f8d870', '#fff8e0'] },
{ name: 'Kodachrome Rouge Red Vert Green', colors: ['#9a3020', '#e06830', '#f8a840', '#f8e870', '#a0c8a0'] },
{ name: 'Disco Violet Purple Rouge Red', colors: ['#1a0030', '#6600cc', '#cc0066', '#ff6600', '#ffcc00'] },
{ name: 'Paisley Violet Purple', colors: ['#2a0840', '#7820a0', '#d050c0', '#f098c0', '#f8d8e8'] },
{ name: 'Daguerreotype Gris Gray', colors: ['#181818', '#484840', '#787860', '#a8a888', '#d8d8c8'] },
{ name: 'Psychedelic Multicolore Multicolor', colors: ['#1a0040', '#8800ff', '#ff0088', '#ff8800', '#88ff00'] },
{ name: 'Faded Marron Brown Gris Gray', colors: ['#a09888', '#b8b0a0', '#d0c8b8', '#e8e0d0', '#f5f0e8'] },
{ name: 'Art Deco Jaune Yellow Noir Black', colors: ['#0a0800', '#4a3800', '#c8a800', '#f8e840', '#fff8e0'] },
// =====================================================================
// INDUSTRIAL & URBAN / INDUSTRIEL & URBAIN (7)
// =====================================================================
{ name: 'Concrete Gris Gray', colors: ['#404040', '#606060', '#808080', '#a0a0a0', '#c0c0c0'] },
{ name: 'Asphalt Gris Gray Noir Black', colors: ['#101010', '#202020', '#383838', '#585858', '#787878'] },
{ name: 'Neon Sign Violet Purple Rouge Red', colors: ['#0a0010', '#3000a0', '#aa00ff', '#ff00aa', '#ff8800'] },
{ name: 'Subway Bleu Blue Gris Gray', colors: ['#101820', '#203040', '#305060', '#408090', '#70a8c0'] },
{ name: 'Oxide Orange Marron Brown', colors: ['#2a1008', '#7a3810', '#c07030', '#e0b060', '#f0d8b0'] },
{ name: 'Titanium Gris Gray', colors: ['#383838', '#585858', '#808080', '#a8a8a8', '#d0d0d0'] },
{ name: 'Foundry Orange Noir Black', colors: ['#100800', '#402000', '#aa5000', '#e09030', '#f8d080'] },
// =====================================================================
// WEATHER & ATMOSPHERE / MÉTÉO & ATMOSPHÈRE (9)
// =====================================================================
{ name: 'Thunderhead Gris Gray Bleu Blue', colors: ['#101820', '#283848', '#406080', '#6088a8', '#a0c0d8'] },
{ name: 'Lightning Bleu Blue Blanc White', colors: ['#050510', '#101030', '#2020a0', '#6060ff', '#ffffff'] },
{ name: 'Rainbow Multicolore Multicolor', colors: ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0088ff', '#8800ff'] },
{ name: 'Fog Bank Gris Gray', colors: ['#202020', '#484848', '#787878', '#a8a8a8', '#d8d8d8'] },
{ name: 'Clear Sky Bleu Blue', colors: ['#e8f4ff', '#d0e8ff', '#b0d0ff', '#80b0ff', '#5090ff'] },
{ name: 'El Nino Bleu Blue Jaune Yellow', colors: ['#0a2030', '#2060a0', '#50a0d0', '#90d0f0', '#f0f890'] },
{ name: 'Trade Wind Bleu Blue Cyan', colors: ['#003058', '#006090', '#30a0c8', '#70d0e8', '#c0f0ff'] },
{ name: 'Permafrost Bleu Blue Blanc White', colors: ['#b0d0f0', '#c8e0f8', '#d8ecff', '#e8f4ff', '#f4faff'] },
{ name: 'Sirocco Jaune Yellow Marron Brown', colors: ['#2a2010', '#6a5818', '#b09830', '#d8c060', '#f0e8b0'] },
// =====================================================================
// CELESTIAL & SPIRITUAL / CÉLESTE & SPIRITUEL (8)
// =====================================================================
{ name: 'Mandala Multicolore Multicolor', colors: ['#c00020', '#d06000', '#f0b800', '#60c020', '#0080c0', '#6000c0'] },
{ name: 'Chakra Multicolore Multicolor', colors: ['#cc0000', '#ff8800', '#ffcc00', '#00cc44', '#0044cc', '#6600cc'] },
{ name: 'Solstice Jaune Yellow Rouge Red', colors: ['#1a0800', '#8a4000', '#f0a000', '#f8e040', '#ffffff'] },
{ name: 'Equinox Vert Green', colors: ['#002010', '#008040', '#00e870', '#80ffb0', '#ffffff'] },
{ name: 'Ether Violet Purple Blanc White', colors: ['#f8f0ff', '#f0e8ff', '#e8e0ff', '#e0d8ff', '#d8d0f8'] },
{ name: 'Astral Violet Purple', colors: ['#080018', '#2000a0', '#8040ff', '#d080ff', '#ffffff'] },
{ name: 'Transcend Violet Purple Blanc White', colors: ['#1a0028', '#5000aa', '#d000ff', '#ff80ff', '#ffffff'] },
// =====================================================================
// CULTURAL & GEOGRAPHIC / CULTUREL & GÉOGRAPHIQUE (7)
// =====================================================================
{ name: 'Kyoto Orange Marron Brown', colors: ['#1a0800', '#8a3020', '#d08050', '#f0c090', '#f8e8d0'] },
{ name: 'Marrakech Rouge Red Jaune Yellow', colors: ['#3a0808', '#b84020', '#e89040', '#f8c870', '#f8f0c0'] },
{ name: 'Santorini Bleu Blue Blanc White', colors: ['#0a1848', '#2050a8', '#80b0e8', '#c0d8f8', '#ffffff'] },
{ name: 'Patagonia Bleu Blue Gris Gray', colors: ['#1a2830', '#305870', '#5090b0', '#90c0d8', '#d0e8f0'] },
{ name: 'Amazon Vert Green', colors: ['#0a1800', '#1a5010', '#30a030', '#70d070', '#b0f8b0'] },
{ name: 'Fjord Bleu Blue', colors: ['#0a1828', '#1a4060', '#305090', '#4888c8', '#90c0e8'] },
{ name: 'Bengal Rouge Red Orange', colors: ['#1a0808', '#8a2820', '#d06038', '#f0a870', '#f8d8c0'] },
// =====================================================================
// CREATIVE EXTRAS / CRÉATIFS SUPPLÉMENTAIRES
// =====================================================================
{ name: 'Glasswork Multicolore Multicolor', colors: ['#e0f0ff', '#f0e0ff', '#ffe0f0', '#fff0e0', '#e0ffe0'] },
{ name: 'Entropy Gris Gray Rose Pink', colors: ['#f8f8f8', '#c0a0a0', '#a0a0c0', '#a0c0a0', '#808080'] },
{ name: 'Umbra Noir Black Gris Gray', colors: ['#000000', '#080808', '#202020', '#484848', '#808080'] },
{ name: 'Prismatic Multicolore Multicolor', colors: ['#ff0044', '#ff8800', '#ffff00', '#00ff88', '#0088ff', '#8800ff'] },
{ name: 'Anti-Gravity Violet Purple Cyan', colors: ['#0a0018', '#180060', '#4800c0', '#c040ff', '#60ffff'] },
{ name: 'Drift Bleu Blue Blanc White', colors: ['#c0d8f0', '#d0e8f8', '#e0f4ff', '#f0faff', '#f8fdff'] },
{ name: 'Tectonic Marron Brown Gris Gray', colors: ['#1a1010', '#504030', '#908060', '#c8c0a0', '#e8e8d8'] },
{ name: 'Black Hole Noir Black Violet Purple', colors: ['#000000', '#0a0005', '#200015', '#500030', '#aa0050'] },
{ name: 'Fracture Bleu Blue Gris Gray', colors: ['#101010', '#404060', '#6060a0', '#9090e0', '#e0e0ff'] },
{ name: 'Soldering Iron Jaune Yellow Orange', colors: ['#1a0800', '#6a2000', '#d06010', '#f0b040', '#ffe880'] },
{ name: 'Ink Wash Noir Black Gris Gray', colors: ['#101010', '#303030', '#606060', '#909090', '#c0c0c0'] },
{ name: 'Fresco Marron Brown Bleu Blue', colors: ['#8a7060', '#b8a888', '#d8c8a8', '#e8d8c0', '#f5eedd'] },
{ name: 'Watercolor Bleu Blue Rose Pink', colors: ['#a0c0e8', '#c0d8f0', '#d0e8f8', '#e8d0f0', '#f8c0d8'] },
{ name: 'Stained Glass Multicolore Multicolor', colors: ['#1a0028', '#4400aa', '#0066cc', '#00aa66', '#ccaa00', '#cc2200'] },
{ name: 'Pop Art Rose Pink Bleu Blue', colors: ['#ff0088', '#ff8800', '#ffff00', '#00ff88', '#0088ff'] },
{ name: 'Minimalist Gris Gray', colors: ['#f0f0f0', '#e0e0e0', '#c0c0c0', '#a0a0a0'] },
{ name: 'Cobalt Coral Bleu Blue Orange', colors: ['#0a1848', '#2050c0', '#f09080'] },
{ name: 'Coral Reef Bleu Blue Orange', colors: ['#001830', '#00405a', '#007080', '#50b090', '#ff8844'] },
{ name: 'Highland Moor Marron Brown Gris Gray', colors: ['#201a10', '#504030', '#887858', '#b8a880', '#d8c8a8'] },
{ name: 'Savanna Jaune Yellow Marron Brown', colors: ['#2a2000', '#7a5a10', '#c09030', '#e0c060', '#f0e8a0'] },
{ name: 'Moss Stone Vert Green Gris Gray', colors: ['#1a1a10', '#3a4020', '#606840', '#90a060', '#c0c890'] },
{ name: 'Old Growth Vert Green', colors: ['#0a1000', '#1a3000', '#304010', '#506020', '#788040'] },
{ name: 'Undergrowth Vert Green', colors: ['#080e00', '#182808', '#305020', '#507840', '#80a860'] },
{ name: 'Wetland Vert Green Marron Brown', colors: ['#1a2808', '#385820', '#607040', '#90a068', '#c0c898'] },
{ name: 'Tundra Gris Gray Bleu Blue', colors: ['#c8d8e8', '#a8c0d8', '#88a8c8', '#6888a8', '#486880'] },
{ name: 'Sahel Jaune Yellow Marron Brown', colors: ['#3a2800', '#8a7010', '#d0b040', '#e8d880', '#f8f5d0'] },
{ name: 'Medina Marron Brown Jaune Yellow', colors: ['#2a1808', '#8a5828', '#c0a060', '#e0d0a0', '#f8f0d8'] },
{ name: 'Orinoco Vert Green', colors: ['#0a1208', '#205030', '#408860', '#70b890', '#b0e8c8'] },
{ name: 'Steppes Vert Green Jaune Yellow', colors: ['#3a3818', '#6a6828', '#a0a040', '#d0d070', '#f0f0a8'] },
{ name: 'Sanctuary Jaune Yellow Marron Brown', colors: ['#1a1810', '#4a4028', '#8a8040', '#c0b870', '#e8e0b0'] },
{ name: 'Sacred Flame Rouge Red Jaune Yellow', colors: ['#1a0000', '#aa2200', '#ff6600', '#ffcc00', '#ffffff'] },
{ name: 'Zenith Bleu Blue Blanc White', colors: ['#000810', '#002060', '#0080c0', '#40c0ff', '#ffffff'] },
{ name: 'Meridian Cyan Blanc White', colors: ['#002030', '#008888', '#00e8e8', '#80ffff', '#ffffff'] },
{ name: 'Nirvana Bleu Blue Blanc White', colors: ['#d8f0ff', '#e0f0ff', '#e8f4ff', '#f0f8ff', '#f8fcff'] },
{ name: 'Rothko Rouge Red Orange', colors: ['#3a0808', '#c02020', '#e86020', '#f0a040'] },
{ name: 'Bauhaus Multicolore Multicolor', colors: ['#cc0000', '#0000cc', '#ffcc00', '#2a2a2a'] },
{ name: 'Expressionist Rouge Red Jaune Yellow', colors: ['#1a0808', '#8a2020', '#e06030', '#f8b060', '#f8e0a0'] },
{ name: 'Impressionist Multicolore Multicolor', colors: ['#d0c890', '#a8c8b8', '#88a8d8', '#b888a8', '#d8a888'] },
{ name: 'Polaroid Gris Gray Blanc White', colors: ['#c0c0b8', '#d8d8d0', '#e8e8e0', '#f0f0e8', '#f8f8f8'] },
{ name: 'Victorian Violet Purple Gris Gray', colors: ['#1a0818', '#5a2850', '#9a6890', '#c8a0c0', '#e8d0e0'] },
{ name: 'Nouveau Vert Green Jaune Yellow', colors: ['#182808', '#4a6020', '#80a050', '#c0c888', '#e8e8c0'] },
{ name: 'Rose Gold Jade Rose Pink Vert Green', colors: ['#d07070', '#e8a898', '#408860', '#80c890'] },
{ name: 'Navy Amber Bleu Blue Jaune Yellow', colors: ['#0a1848', '#1a3a80', '#c89030', '#f8d060'] },
{ name: 'Indigo Coral Violet Purple Orange', colors: ['#1a0840', '#5030a0', '#e07060', '#f8b090'] },
{ name: 'Teal Wine Cyan Rouge Red', colors: ['#1a4040', '#20a090', '#800830', '#c04060'] },
{ name: 'Mint Flame Vert Green Rouge Red', colors: ['#208868', '#60d0a8', '#d04010', '#f07040'] },
{ name: 'Slate Peach Gris Gray Orange', colors: ['#3a4858', '#6888a8', '#e8a870', '#f8d0a8'] },
{ name: 'Lilac Gold Violet Purple Jaune Yellow', colors: ['#8850b8', '#c090e0', '#c09020', '#f0d860'] },
{ name: 'Forest Sunset Vert Green Rouge Red', colors: ['#1a4010', '#407030', '#e07020', '#f8c060'] },
// =====================================================================
// NEW / NOUVEAUX — ROUGES & FEUX (20)
// =====================================================================
{ name: 'Dragon Scale Rouge Red', colors: ['#1a0000', '#5a0000', '#c00020', '#ff2020', '#ff9040'] },
{ name: 'Wildfire Rouge Red Orange', colors: ['#200000', '#881000', '#e04000', '#ff8000', '#ffdd00'] },
{ name: 'Lava Crust Rouge Red', colors: ['#0a0000', '#300000', '#800000', '#cc3300', '#ff6620'] },
{ name: 'Burning Rose Rouge Red Rose Pink', colors: ['#400010', '#a00030', '#e02060', '#f87090', '#ffb8cc'] },
{ name: 'Cherry Coal Rouge Red Noir Black', colors: ['#100000', '#400000', '#8a1020', '#cc3040'] },
{ name: 'Flamethrower Rouge Red Jaune Yellow', colors: ['#1a0000', '#7a1000', '#dd4000', '#ff8800', '#ffe000'] },
{ name: 'Coral Sunset Rouge Red Orange', colors: ['#3a0010', '#aa2820', '#e06040', '#f0a060', '#f8e0b0'] },
{ name: 'Magenta Blaze Rouge Red Violet Purple', colors: ['#200010', '#800040', '#e00080', '#ff40b0', '#ffaae0'] },
{ name: 'War Paint Rouge Red', colors: ['#120000', '#5a0808', '#a82020', '#d85030'] },
{ name: 'Red Velvet Rouge Red', colors: ['#1a0008', '#6a0018', '#b81030', '#e06070', '#f8b8c0'] },
{ name: 'Iron Heat Rouge Red Orange', colors: ['#100000', '#3a0800', '#8a2800', '#d06000', '#f8c000'] },
{ name: 'Habanero Rouge Red Orange', colors: ['#2a0000', '#8a1000', '#d03000', '#f07000', '#f8a030'] },
{ name: 'Caldera Rouge Red', colors: ['#080000', '#280000', '#700000', '#c82000', '#ff6000'] },
{ name: 'Rose Flame Rouge Red Rose Pink', colors: ['#2a0018', '#8a0040', '#d03060', '#f06090', '#ffb0c0'] },
{ name: 'Lychee Rouge Red Rose Pink', colors: ['#3a0010', '#880040', '#d06070', '#f0a0a8', '#f8d8d8'] },
{ name: 'Ember Cave Rouge Red Marron Brown', colors: ['#100400', '#402010', '#8a4020', '#c07040', '#e0a870'] },
{ name: 'Cerise Smoke Rouge Red', colors: ['#1a0008', '#700028', '#c03050', '#e07080'] },
{ name: 'Torch Rouge Red Jaune Yellow', colors: ['#1a0000', '#600000', '#cc2000', '#f06000', '#ffb800'] },
{ name: 'Sunset Embers Rouge Red Orange Jaune Yellow', colors: ['#0a0000', '#4a0800', '#b04000', '#e08000', '#f8cc00', '#fffff0'] },
{ name: 'Red Shift Rouge Red Violet Purple', colors: ['#050010', '#280040', '#7a0088', '#cc0044', '#ff4000'] },
// =====================================================================
// NEW / NOUVEAUX — VERTS (20)
// =====================================================================
{ name: 'Swamp Vert Green Marron Brown', colors: ['#081008', '#203818', '#406030', '#608850', '#90b870'] },
{ name: 'Chlorophyll Vert Green', colors: ['#002800', '#007000', '#30c030', '#90f060', '#e0ffa0'] },
{ name: 'Lime Sherbet Vert Green Jaune Yellow', colors: ['#e0ff90', '#c8f870', '#a8f050', '#80e030', '#60c010'] },
{ name: 'Neon Moss Vert Green', colors: ['#001000', '#003000', '#008800', '#40dd00', '#aaff40'] },
{ name: 'Seagrass Vert Green Cyan', colors: ['#001808', '#005030', '#00a860', '#50d890', '#b0f8c0'] },
{ name: 'Canopy Glow Vert Green Jaune Yellow', colors: ['#0a1800', '#2a5010', '#50a020', '#90d040', '#d0f890'] },
{ name: 'Poison Ivy Vert Green', colors: ['#001800', '#005020', '#20a040', '#70e070', '#c8ffc0'] },
{ name: 'Tea Garden Vert Green', colors: ['#283818', '#4a6828', '#70a040', '#a8c878', '#d8eeb0'] },
{ name: 'Glacier Mint Vert Green Cyan', colors: ['#b0f0d8', '#90e0c8', '#70d0b8', '#50c0a8', '#30b098'] },
{ name: 'Pesto Vert Green Jaune Yellow', colors: ['#283010', '#506028', '#809040', '#b0c068', '#d8e0a0'] },
{ name: 'Green Flash Vert Green', colors: ['#002000', '#006020', '#00c850', '#60ff90', '#ccffe0'] },
{ name: 'Phytoplankton Vert Green Cyan', colors: ['#002020', '#006050', '#00c888', '#60ffc0', '#ccffee'] },
{ name: 'Cactus Vert Green', colors: ['#1a2810', '#3a5820', '#609038', '#90c060', '#c8e8a0'] },
{ name: 'Irish Mist Vert Green', colors: ['#102008', '#306030', '#60a058', '#98c880', '#d0f0b8'] },
{ name: 'Verdigris Vert Green Cyan', colors: ['#102820', '#308060', '#50c098', '#90e8c8', '#d0fff0'] },
{ name: 'Mangrove Vert Green Marron Brown', colors: ['#101808', '#304028', '#507840', '#80a860', '#b8d8a0'] },
{ name: 'Mojito Vert Green Jaune Yellow', colors: ['#203810', '#508028', '#80c040', '#b8e868', '#e8ffb0'] },
{ name: 'Velvet Moss Vert Green', colors: ['#0a1200', '#203808', '#407020', '#68a840', '#a0d870'] },
// =====================================================================
// NEW / NOUVEAUX — BLEUS (20)
// =====================================================================
{ name: 'Midnight Ocean Bleu Blue', colors: ['#000308', '#000818', '#001838', '#003060', '#0060a0'] },
{ name: 'Steel Blue Bleu Blue Gris Gray', colors: ['#101828', '#203858', '#406898', '#6090c0', '#a0c0e8'] },
{ name: 'Arctic Deep Bleu Blue', colors: ['#000820', '#002050', '#003880', '#006bbf', '#40a0e0'] },
{ name: 'Blue Morpho Bleu Blue', colors: ['#000520', '#001060', '#0040c0', '#2090f8', '#80d0ff'] },
{ name: 'Ocean Trench Bleu Blue', colors: ['#000005', '#000015', '#000838', '#001868', '#0038a0'] },
{ name: 'Cerulean Bleu Blue', colors: ['#001838', '#004888', '#0098e0', '#50c8f8', '#b0eaff'] },
{ name: 'Denim Bleu Blue', colors: ['#0a1828', '#204060', '#306898', '#5090c0', '#88b8e0'] },
{ name: 'Hydrothermal Bleu Blue Cyan', colors: ['#000818', '#003060', '#0068b0', '#00b8d0', '#80f0ff'] },
{ name: 'Blueprint Bleu Blue', colors: ['#000820', '#001848', '#002870', '#00409a', '#3080d0'] },
{ name: 'Ink River Bleu Blue Violet Purple', colors: ['#050010', '#100838', '#280870', '#4820a8', '#8060e0'] },
{ name: 'Frostbite Bleu Blue Blanc White', colors: ['#c8e8ff', '#d8f0ff', '#e8f8ff', '#f0fcff', '#ffffff'] },
{ name: 'Periwinkle Bleu Blue Violet Purple', colors: ['#181848', '#404898', '#6870c8', '#98a8e0', '#d0d8f8'] },
{ name: 'Twilight Sea Bleu Blue Violet Purple', colors: ['#080828', '#181858', '#302898', '#5050c8', '#8890e8'] },
{ name: 'Selene Bleu Blue', colors: ['#0a0a20', '#182050', '#304898', '#6080d0', '#b0c8f0'] },
{ name: 'Icefield Bleu Blue Blanc White', colors: ['#b8d8f8', '#c8e4ff', '#d8eeff', '#e8f4ff', '#f4f9ff'] },
{ name: 'Neptunian Bleu Blue', colors: ['#000818', '#001048', '#002890', '#1060c8', '#50a0f0'] },
{ name: 'Blue Flame Bleu Blue Cyan', colors: ['#000020', '#000880', '#0040ff', '#00c8ff', '#80ffff'] },
{ name: 'Bay Fog Bleu Blue Gris Gray', colors: ['#a8b8c8', '#c0d0e0', '#d8e8f0', '#e8f0f8', '#f4f8fc'] },
{ name: 'Tidewater Bleu Blue', colors: ['#001828', '#004060', '#007898', '#30b0d0', '#80d8f0'] },
{ name: 'Starlight Bleu Blue Blanc White', colors: ['#080818', '#182040', '#3840a0', '#7080e0', '#d0d8ff'] },
// =====================================================================
// NEW / NOUVEAUX — JAUNES & ORS (20)
// =====================================================================
{ name: 'Sunflower Jaune Yellow', colors: ['#3a2000', '#9a6010', '#e0b020', '#f8e040', '#fffbc0'] },
{ name: 'Champagne Jaune Yellow Blanc White', colors: ['#e8d8a0', '#f0e4b8', '#f8f0d8', '#fcf8f0', '#fffff8'] },
{ name: 'Lemon Drop Jaune Yellow', colors: ['#fffce0', '#fff8c0', '#fff090', '#ffe060', '#ffc820'] },
{ name: 'Wheat Field Jaune Yellow Marron Brown', colors: ['#3a2808', '#8a6818', '#d0a030', '#f0c860', '#f8f0d0'] },
{ name: 'Neon Lemon Jaune Yellow Vert Green', colors: ['#e0ff00', '#c8f000', '#a8e000', '#80c800', '#58a000'] },
{ name: 'Antiqued Gold Jaune Yellow Marron Brown', colors: ['#2a1800', '#6a4808', '#b08830', '#d8c068', '#f0e8b8'] },
{ name: 'Electric Lime Jaune Yellow Vert Green', colors: ['#003000', '#409000', '#a0f000', '#d8ff40', '#f8ffa0'] },
{ name: 'Old Gold Jaune Yellow', colors: ['#2a2000', '#7a6000', '#c8a820', '#e8d060', '#f8f0c0'] },
{ name: 'Canary Jaune Yellow', colors: ['#f8f080', '#f8e850', '#f8d820', '#e8c000', '#c8a000'] },
{ name: 'Chartreuse Jaune Yellow Vert Green', colors: ['#303800', '#608000', '#a0c000', '#d0f020', '#e8ff80'] },
{ name: 'Coin Jaune Yellow', colors: ['#3a2800', '#8a6010', '#d0a820', '#f0d840', '#f8f8d0'] },
{ name: 'Midas Touch Jaune Yellow', colors: ['#1a0800', '#7a4800', '#d08000', '#f0c020', '#fff8a0'] },
{ name: 'Amber Wave Jaune Yellow Orange', colors: ['#2a1000', '#8a4000', '#d08020', '#f0b840', '#f8e8c0'] },
{ name: 'Vanilla Jaune Yellow Blanc White', colors: ['#f8f0d0', '#f8ecc0', '#f8e8b0', '#f4e098', '#ecd880'] },
{ name: 'Solar Noon Jaune Yellow', colors: ['#fffff0', '#ffff80', '#ffe820', '#ffc000', '#ff9800'] },
{ name: 'Pollen Jaune Yellow Vert Green', colors: ['#d0e880', '#e0f060', '#f0f840', '#f8f020', '#f8e000'] },
{ name: 'Gilded Rose Jaune Yellow Rose Pink', colors: ['#4a2018', '#a05828', '#e0b860', '#f8d888', '#f8f0c8'] },
{ name: 'Dandelion Jaune Yellow', colors: ['#f8e870', '#f8d040', '#f8b810', '#e8a000', '#c07000'] },
{ name: 'Vintage Ivory Jaune Yellow Blanc White', colors: ['#f8f4e8', '#f8f0d8', '#f0e8c0', '#e8d8a0', '#d8c880'] },
{ name: 'Harvest Gold Jaune Yellow Orange', colors: ['#2a1400', '#7a4000', '#c08020', '#e8b040', '#f8e090'] },
// =====================================================================
// NEW / NOUVEAUX — VIOLETS (20)
// =====================================================================
{ name: 'Ultraviolet Violet Purple', colors: ['#050018', '#180060', '#5000c8', '#b000ff', '#e860ff'] },
{ name: 'Mauve Violet Purple Rose Pink', colors: ['#281030', '#703860', '#b07898', '#d8a8c0', '#f0d8e8'] },
{ name: 'Grape Crush Violet Purple', colors: ['#1a0028', '#600080', '#a040c0', '#d080e0', '#f0c0f8'] },
{ name: 'Twilight Violet Purple Bleu Blue', colors: ['#080028', '#201060', '#502098', '#8048d0', '#c090f0'] },
{ name: 'Elderberry Violet Purple', colors: ['#150018', '#480048', '#800060', '#c03080', '#e888b8'] },
{ name: 'Dark Orchid Violet Purple', colors: ['#0a0018', '#300058', '#7800aa', '#c040e0', '#f090ff'] },
{ name: 'Pastel Lavender Violet Purple Blanc White', colors: ['#e0d8f8', '#e8e0ff', '#f0e8ff', '#f8f0ff', '#ffffff'] },
{ name: 'Velvet Violet Purple', colors: ['#0a0010', '#2a0048', '#6800a8', '#b040e0', '#e880ff'] },
{ name: 'Heliotrope Violet Purple Rose Pink', colors: ['#280030', '#800080', '#d040d0', '#f880f0', '#ffc0ff'] },
{ name: 'Byzantine Violet Purple', colors: ['#180028', '#500068', '#9000c0', '#d040e8', '#f890ff'] },
{ name: 'Iris Violet Purple Bleu Blue', colors: ['#180030', '#480080', '#7030c0', '#9868e0', '#c0a0f8'] },
{ name: 'Neon Grape Violet Purple', colors: ['#100020', '#400080', '#9000ff', '#cc60ff', '#ee80ff'] },
{ name: 'Prune Violet Purple', colors: ['#180018', '#500038', '#880050', '#c06880', '#e8a8b8'] },
{ name: 'Purple Dusk Violet Purple Bleu Blue', colors: ['#080020', '#200858', '#500898', '#8840c8', '#c090e8'] },
{ name: 'Morpho Violet Purple Bleu Blue', colors: ['#080018', '#200058', '#4818a8', '#8060d8', '#c0b0ff'] },
{ name: 'Aubergine Violet Purple', colors: ['#0a0008', '#300018', '#600038', '#9a4060', '#c88090'] },
{ name: 'Lilac Dream Violet Purple Rose Pink', colors: ['#281838', '#785898', '#c0a0d8', '#e8d0f0', '#f8f0ff'] },
{ name: 'Frozen Violet Violet Purple Bleu Blue', colors: ['#d0d0f8', '#c0c8ff', '#b0c0ff', '#a0b8ff', '#90a8f8'] },
// =====================================================================
// NEW / NOUVEAUX — ROSES & PÊCHES (20)
// =====================================================================
{ name: 'Neon Pink Rose Pink', colors: ['#200010', '#900050', '#ff00a0', '#ff70d0', '#ffb0e8'] },
{ name: 'Flamingo Rose Pink', colors: ['#e87878', '#f0a0a0', '#f8c8c0', '#f8e0d8', '#fff0f0'] },
{ name: 'Strawberry Cream Rose Pink', colors: ['#400020', '#c03060', '#e87898', '#f8b8c0', '#fff0f5'] },
{ name: 'Deep Magenta Rose Pink Violet Purple', colors: ['#1a0010', '#700050', '#c00090', '#e850c0', '#f8a0e0'] },
{ name: 'Blush Peach Rose Pink Orange', colors: ['#f0c8b8', '#f8d8c8', '#f8e4d8', '#f8eee8', '#fff8f5'] },
{ name: 'Peony Rose Pink', colors: ['#2a0018', '#8a2848', '#d05878', '#f090a8', '#f8d0d8'] },
{ name: 'Dusty Rose Rose Pink', colors: ['#3a1828', '#8a5060', '#c08898', '#e0b8c0', '#f8e0e8'] },
{ name: 'Nectarine Rose Pink Orange', colors: ['#e08060', '#f0a070', '#f8c888', '#f8e0a0', '#fff8d0'] },
{ name: 'Electric Rose Rose Pink', colors: ['#1a0018', '#700060', '#d000a0', '#ff50d0', '#ffa8f0'] },
{ name: 'Cosmo Rose Pink Violet Purple', colors: ['#200020', '#800060', '#d040a0', '#f080d0', '#f8c8f0'] },
{ name: 'Guava Rose Pink Orange', colors: ['#3a0010', '#a83040', '#e07060', '#f8a888', '#f8d8c8'] },
{ name: 'Ballet Rose Pink', colors: ['#e8c8d0', '#f0d8e0', '#f8e8f0', '#f8f0f8', '#ffffff'] },
{ name: 'Sunset Rose Rose Pink Orange', colors: ['#2a0010', '#901040', '#e06060', '#f0a080', '#f8e0c0'] },
{ name: 'Powder Rose Pink Blanc White', colors: ['#f0d8e8', '#f8e4f0', '#f8ecf4', '#fef4f8', '#ffffff'] },
{ name: 'Crimson Rose Rouge Red Rose Pink', colors: ['#1a0010', '#700030', '#c02050', '#f06080', '#f8b0c0'] },
{ name: 'Sorbet Rose Pink', colors: ['#ff99bb', '#ffaacc', '#ffbbdd', '#ffccee', '#ffeeff'] },
{ name: 'Tutti Frutti Rose Pink Multicolore', colors: ['#ff6699', '#ff9966', '#ffcc66', '#ccff66', '#66ccff'] },
{ name: 'Rose Gold Shimmer Rose Pink Jaune Yellow', colors: ['#8a3848', '#c07888', '#e0c0a8', '#f8e0c0', '#f8f4e8'] },
{ name: 'Petal Rose Pink', colors: ['#f8e8f0', '#f0d0e4', '#e8b8d8', '#d898c0', '#c070a0'] },
{ name: 'Taffy Rose Pink Bleu Blue', colors: ['#ff99cc', '#ee88ff', '#9988ff', '#88ccff', '#99ffee'] },
// =====================================================================
// NEW / NOUVEAUX — ORANGES & AMBERS (20)
// =====================================================================
{ name: 'Papaya Orange', colors: ['#3a1400', '#a04800', '#e08020', '#f8b840', '#f8e8a0'] },
{ name: 'Pumpkin Orange', colors: ['#2a1000', '#8a3800', '#d06818', '#f0a040', '#f8d898'] },
{ name: 'Amber Resin Orange Marron Brown', colors: ['#2a1000', '#8a5020', '#d09030', '#f0c870', '#f8f0d8'] },
{ name: 'Saffron Dusk Orange Jaune Yellow', colors: ['#3a1800', '#a04800', '#e08800', '#f8c820', '#fff880'] },
{ name: 'Apricot Orange Rose Pink', colors: ['#3a1800', '#a05030', '#e09060', '#f8c090', '#f8e0c8'] },
{ name: 'Sienna Orange Marron Brown', colors: ['#2a0a00', '#8a3010', '#c06030', '#e09060', '#f8c8a8'] },
{ name: 'Peach Fuzz Orange Rose Pink', colors: ['#f8d0a0', '#f8e0b8', '#f8e8d0', '#f8f0e8', '#fff8f4'] },
{ name: 'Clementine Orange', colors: ['#3a0000', '#c03000', '#f07000', '#f8a820', '#f8e080'] },
{ name: 'Sherbet Orange Rose Pink', colors: ['#f08040', '#f8a868', '#f8c898', '#f8d8b8', '#f8e8d8'] },
{ name: 'Hot Sauce Orange Rouge Red', colors: ['#2a0000', '#880800', '#d04000', '#f07820', '#f8b850'] },
{ name: 'Marmalade Orange Jaune Yellow', colors: ['#3a0800', '#b04010', '#e08030', '#f8b840', '#f8e880'] },
{ name: 'Amber Alert Orange', colors: ['#2a0800', '#8a3800', '#e08000', '#f8b020', '#f8f860'] },
{ name: 'Cider Orange Marron Brown', colors: ['#2a1000', '#7a3808', '#c07828', '#e0a850', '#f0d898'] },
{ name: 'Marigold Orange Jaune Yellow', colors: ['#3a1800', '#a85808', '#e09818', '#f8c840', '#f8f0a0'] },
{ name: 'Tiger Lily Orange', colors: ['#2a0800', '#9a2800', '#e06020', '#f89050', '#f8c8a8'] },
{ name: 'Neon Orange Orange', colors: ['#1a0800', '#700000', '#dd4000', '#ff8000', '#ffcc00'] },
{ name: 'Persimmon Orange Rouge Red', colors: ['#3a0800', '#a02810', '#d85828', '#f09060', '#f8c8a8'] },
{ name: 'Caramel Macchiato Orange Marron Brown', colors: ['#2a1000', '#7a4020', '#c08040', '#e8b870', '#f8e4c0'] },
// =====================================================================
// NEW / NOUVEAUX — CYANS & TEALS (20)
// =====================================================================
{ name: 'Deep Teal Cyan', colors: ['#001818', '#004848', '#008888', '#30c0b8', '#a0f0e8'] },
{ name: 'Electric Cyan Cyan', colors: ['#001018', '#003050', '#0080c0', '#00e8ff', '#a0ffff'] },
{ name: 'Mermaid Cyan Vert Green', colors: ['#001818', '#006050', '#00b888', '#50e8b8', '#b0fff0'] },
{ name: 'Neon Aqua Cyan', colors: ['#000808', '#002828', '#009090', '#00f0e8', '#80fff8'] },
{ name: 'Marine Phosphor Cyan Bleu Blue', colors: ['#000818', '#002850', '#0060a0', '#20c0e0', '#a0fff8'] },
{ name: 'Submarine Cyan Bleu Blue', colors: ['#001018', '#003038', '#006070', '#20a0b0', '#70d8e8'] },
{ name: 'Abalone Cyan Multicolore', colors: ['#b0d8d0', '#b8d0e0', '#c8c8f0', '#d8c0f0', '#e8d0f0'] },
{ name: 'Patina Teal Cyan Vert Green', colors: ['#102820', '#305040', '#508868', '#80b898', '#b8e0c8'] },
{ name: 'Hummingbird Cyan Vert Green', colors: ['#002818', '#008060', '#20c0a0', '#80e8d0', '#d0fff8'] },
{ name: 'Caspian Cyan Bleu Blue', colors: ['#001828', '#005070', '#0090b0', '#40c0d8', '#a0eef8'] },
{ name: 'Spearmint Cyan Vert Green', colors: ['#003820', '#008050', '#30c888', '#90f8c0', '#d0fff0'] },
{ name: 'Glaucous Cyan Bleu Blue', colors: ['#183848', '#307088', '#50a8c0', '#80d0e0', '#c0f0f8'] },
{ name: 'Night Aqua Cyan Noir Black', colors: ['#001010', '#003030', '#006868', '#00c8c8', '#80ffff'] },
{ name: 'Celeste Cyan Blanc White', colors: ['#b0e8f0', '#c8f0f8', '#d8f8ff', '#eafeff', '#f8ffff'] },
{ name: 'Lagoon Cyan Vert Green', colors: ['#001820', '#006070', '#20a8a0', '#70d8d0', '#c0fff8'] },
{ name: 'Arcadia Cyan Vert Green', colors: ['#002010', '#007858', '#30d0a8', '#90f0d8', '#d8fff8'] },
{ name: 'Brine Cyan', colors: ['#001018', '#003040', '#006070', '#40a8b8', '#80d8e8'] },
{ name: 'Aquamarine Cyan Vert Green', colors: ['#002820', '#008070', '#30c8b0', '#80f0e0', '#d0fffc'] },
// =====================================================================
// NEW / NOUVEAUX — GRAYS & BLACKS (15)
// =====================================================================
{ name: 'Graphite Gris Gray', colors: ['#101010', '#282828', '#484848', '#707070', '#a0a0a0'] },
{ name: 'Storm Cloud Gris Gray Bleu Blue', colors: ['#181828', '#303848', '#506070', '#7898b0', '#a8c8d8'] },
{ name: 'Ash Gris Gray', colors: ['#202018', '#484838', '#707058', '#989870', '#b8b898'] },
{ name: 'Volcanic Ash Gris Gray', colors: ['#101010', '#282820', '#484840', '#686858', '#888870'] },
{ name: 'Slate Gris Gray Bleu Blue', colors: ['#1a2028', '#303848', '#507078', '#7090a0', '#a0b8c0'] },
{ name: 'Moonlight Gris Gray', colors: ['#282838', '#484858', '#686880', '#9898b0', '#c8c8d8'] },
{ name: 'Flint Spark Gris Gray Jaune Yellow', colors: ['#202020', '#484840', '#909080', '#d0d0c0', '#f8f8d8'] },
{ name: 'Matte Black Noir Black', colors: ['#0a0a0a', '#141414', '#1e1e1e', '#282828', '#323232'] },
{ name: 'Steel Gris Gray', colors: ['#202028', '#383840', '#585868', '#808090', '#b0b0c0'] },
// =====================================================================
// NEW / NOUVEAUX — WHITES & CREAMS (10)
// =====================================================================
{ name: 'Pearl Blanc White', colors: ['#f0f0f8', '#f4f4fc', '#f8f8ff', '#fcfcff', '#ffffff'] },
{ name: 'Cream Blanc White Jaune Yellow', colors: ['#f8f4e0', '#f8f0d8', '#f8ecc8', '#f0e8b8', '#e8e0a8'] },
{ name: 'Parchment Blanc White Marron Brown', colors: ['#f0e8d0', '#e8e0c8', '#e0d8b8', '#d8d0a8', '#d0c898'] },
{ name: 'Ivory Blanc White', colors: ['#f8f4e8', '#f4f0e0', '#f0ecd8', '#ece8d0', '#e4e0c8'] },
{ name: 'Cloud Blanc White Bleu Blue', colors: ['#f4f8ff', '#e8f0ff', '#dceaff', '#d0e4ff', '#c4deff'] },
{ name: 'Egret Blanc White Gris Gray', colors: ['#f8f8f4', '#f0f0ec', '#e8e8e0', '#e0e0d8', '#d8d8d0'] },
{ name: 'Magnolia Blanc White Rose Pink', colors: ['#f8f4f8', '#f4ecf4', '#f0e4f0', '#e8dce8', '#e0d4e0'] },
{ name: 'Frost Blanc White Cyan', colors: ['#f0f8ff', '#e4f4ff', '#d8f0ff', '#ccecff', '#c0e8ff'] },
// =====================================================================
// NEW / NOUVEAUX — COSMIC & SCI-FI (20)
// =====================================================================
{ name: 'Nebula Core Violet Purple Rose Pink', colors: ['#080018', '#300060', '#8000d0', '#e040e0', '#ff90f0'] },
{ name: 'Cryogenic Bleu Blue Cyan', colors: ['#000820', '#002060', '#008888', '#40e0e0', '#c0ffff'] },
{ name: 'Xenon Cyan Violet Purple', colors: ['#000020', '#001060', '#2000c0', '#6000ff', '#00ffff'] },
{ name: 'Plasma Core Violet Purple Jaune Yellow', colors: ['#050010', '#2000a0', '#8800ff', '#ff8800', '#ffff00'] },
{ name: 'Quantum Foam Bleu Blue Violet Purple', colors: ['#000818', '#082070', '#3050b8', '#8090e0', '#e0e0ff'] },
{ name: 'Antimatter Violet Purple Noir Black', colors: ['#000000', '#100020', '#400080', '#9000d0', '#e040ff'] },
{ name: 'Neutron Star Blanc White Bleu Blue', colors: ['#ffffff', '#d0e8ff', '#a0c0ff', '#6088ff', '#2040ff'] },
{ name: 'Parsec Bleu Blue Violet Purple', colors: ['#000010', '#000840', '#0018a0', '#4030e0', '#c080ff'] },
{ name: 'Stellar Nursery Rose Pink Violet Purple', colors: ['#180038', '#600080', '#c040c0', '#f070d0', '#ffc0f0'] },
{ name: 'Hawking Radiation Jaune Yellow Blanc White', colors: ['#1a0800', '#884800', '#f0c000', '#f8f060', '#ffffff'] },
{ name: 'Singularity Violet Purple Noir Black', colors: ['#000000', '#050005', '#150015', '#350035', '#700070'] },
{ name: 'Chromosphere Rouge Red Jaune Yellow', colors: ['#100000', '#4a0000', '#b02000', '#f06000', '#ffe000'] },
{ name: 'Interstellar Bleu Blue', colors: ['#000008', '#000020', '#001060', '#0040c0', '#20a0ff'] },
{ name: 'Pulsar Wind Cyan Violet Purple', colors: ['#000010', '#001050', '#0080c0', '#c000ff', '#ff80ff'] },
{ name: 'Red Dwarf Rouge Red Marron Brown', colors: ['#0a0000', '#380a00', '#882000', '#c06020', '#e0a070'] },
{ name: 'Cosmic Ray Cyan Blanc White', colors: ['#001020', '#0040a0', '#00c0f0', '#80f0ff', '#ffffff'] },
{ name: 'Magnetar Violet Purple Cyan', colors: ['#050015', '#180058', '#5010b0', '#0080ff', '#00ffcc'] },
{ name: 'Space Station Gris Gray Bleu Blue', colors: ['#101828', '#283848', '#406080', '#6090b0', '#90c0d8'] },
{ name: 'Ion Drive Bleu Blue Cyan', colors: ['#000020', '#002080', '#00a0ff', '#80f0ff', '#ffffff'] },
{ name: 'Dark Nebula Violet Purple', colors: ['#030005', '#100018', '#280038', '#580068', '#9808a8'] },
// =====================================================================
// NEW / NOUVEAUX — NATURE & TERROIR (20)
// =====================================================================
{ name: 'Volcanic Soil Marron Brown Noir Black', colors: ['#0a0800', '#281808', '#503020', '#885040', '#b08870'] },
{ name: 'Basalt Gris Gray Noir Black', colors: ['#080808', '#181810', '#282818', '#404030', '#585848'] },
{ name: 'Prairie Wind Vert Green Jaune Yellow', colors: ['#283818', '#508038', '#88b850', '#c0d878', '#e8f0b0'] },
{ name: 'Glacier Bleu Blue Blanc White', colors: ['#c0d8f0', '#d0e8f8', '#e0f0ff', '#f0f8ff', '#f8fcff'] },
{ name: 'Riverbed Marron Brown Gris Gray', colors: ['#1a1808', '#485028', '#788858', '#a8b880', '#d0d8b0'] },
{ name: 'Loam Marron Brown', colors: ['#201008', '#604028', '#a07050', '#c8a880', '#e8d0b8'] },
{ name: 'Hot Spring Cyan Vert Green', colors: ['#001818', '#007070', '#20c0b0', '#80f8e0', '#d0fff8'] },
{ name: 'Lichen Vert Green Gris Gray', colors: ['#202818', '#486040', '#709868', '#a0c098', '#d0e8c8'] },
{ name: 'Tide Flat Gris Gray Cyan', colors: ['#182028', '#305060', '#508890', '#80b8c8', '#b0d8e8'] },
{ name: 'Kelp Forest Vert Green', colors: ['#001808', '#005030', '#208060', '#50b890', '#a0e8c8'] },
{ name: 'Bark Marron Brown', colors: ['#1a0800', '#502010', '#8a4020', '#b06840', '#d09878'] },
{ name: 'Moss Rock Vert Green Gris Gray', colors: ['#181810', '#384830', '#607050', '#90a070', '#c0c8a0'] },
{ name: 'Fossil Gris Gray Marron Brown', colors: ['#2a2820', '#585040', '#888068', '#b0a890', '#d8d0b8'] },
{ name: 'Soil Layer Marron Brown', colors: ['#100800', '#402810', '#804830', '#b08060', '#d8b898'] },
{ name: 'Creek Bed Bleu Blue Marron Brown', colors: ['#1a2028', '#405060', '#607880', '#88a8b0', '#c0d0d8'] },
{ name: 'Mushroom Marron Brown Gris Gray', colors: ['#2a2018', '#605848', '#988870', '#c0b098', '#e0d8c8'] },
{ name: 'Quartzite Gris Gray Blanc White', colors: ['#c0c0c8', '#d0d0d8', '#e0e0e8', '#f0f0f8', '#f8f8ff'] },
{ name: 'Mineral Spring Cyan Vert Green', colors: ['#001818', '#006858', '#30b8a0', '#80e8d8', '#d0fff8'] },
{ name: 'Dew Drop Vert Green Blanc White', colors: ['#d0f8e0', '#e0f8f0', '#f0fef8', '#f8fffd', '#ffffff'] },
{ name: 'Peat Marron Brown Noir Black', colors: ['#0a0800', '#201808', '#402818', '#604030', '#885848'] },
// =====================================================================
// NEW / NOUVEAUX — FOOD & DRINK (15)
// =====================================================================
{ name: 'Matcha Vert Green', colors: ['#283818', '#508040', '#88b868', '#c0d898', '#e8f8c8'] },
{ name: 'Blueberry Violet Purple Bleu Blue', colors: ['#0a0818', '#281840', '#502880', '#7850a8', '#b0a0d0'] },
{ name: 'Raspberry Jam Rouge Red Rose Pink', colors: ['#1a0008', '#700030', '#c84060', '#f08098', '#f8c0d0'] },
{ name: 'Dark Chocolate Marron Brown Noir Black', colors: ['#0a0400', '#200c00', '#401808', '#682010', '#902818'] },
{ name: 'Mint Chocolate Vert Green Marron Brown', colors: ['#101808', '#305828', '#60a848', '#90d870', '#c0f8a0'] },
{ name: 'Lavender Honey Violet Purple Jaune Yellow', colors: ['#302848', '#708098', '#c0b8d8', '#e8d890', '#f8f0b8'] },
{ name: 'Espresso Cream Marron Brown Blanc White', colors: ['#180800', '#603818', '#c09858', '#e8d0a0', '#f8f0e0'] },
{ name: 'Mango Lassi Orange Jaune Yellow', colors: ['#3a1400', '#b05818', '#e8a840', '#f8d880', '#fff8d0'] },
{ name: 'Grenadine Rouge Red Orange', colors: ['#2a0000', '#900000', '#e04000', '#f88020', '#ffc860'] },
{ name: 'Pistachio Vert Green Jaune Yellow', colors: ['#2a3818', '#607048', '#98a870', '#c8d0a0', '#e8f0d0'] },
{ name: 'Blue Cheese Bleu Blue Gris Gray', colors: ['#3a3848', '#6a7080', '#a0a8c0', '#c8d0e0', '#e8ecf8'] },
{ name: 'Sour Apple Vert Green Jaune Yellow', colors: ['#183800', '#409810', '#80d820', '#c0f840', '#e8ff90'] },
{ name: 'Black Sesame Noir Black Gris Gray', colors: ['#101010', '#201818', '#382820', '#504030', '#686050'] },
{ name: 'Pomelo Rose Pink Jaune Yellow', colors: ['#f8e8c0', '#f8d8a8', '#f8c890', '#f8a878', '#f8806a'] },
{ name: 'Tahini Jaune Yellow Marron Brown', colors: ['#3a2808', '#8a6820', '#c0a848', '#d8c890', '#f0e8d0'] },
// =====================================================================
// NEW / NOUVEAUX — ABSTRACT & ARTISTIC (20)
// =====================================================================
{ name: 'Chiaroscuro Noir Black Blanc White', colors: ['#000000', '#181818', '#606060', '#b8b8b8', '#ffffff'] },
{ name: 'Negative Space Noir Black Blanc White', colors: ['#080808', '#303030', '#787878', '#d0d0d0', '#f8f8f8'] },
{ name: 'Oxidized Copper Orange Vert Green', colors: ['#1a0800', '#7a3818', '#b06020', '#408060', '#60a888'] },
{ name: 'Neon Void Violet Purple Cyan', colors: ['#000008', '#000030', '#0000c0', '#8000ff', '#00ffff'] },
{ name: 'Thermal Scan Bleu Blue Rouge Red', colors: ['#000040', '#0030a0', '#008888', '#a04000', '#ff2000'] },
{ name: 'Pixel Burn Violet Purple Orange', colors: ['#050010', '#280060', '#9800ff', '#ff8000', '#ffff00'] },
{ name: 'Glitch Art Rose Pink Bleu Blue', colors: ['#ff0088', '#ff0000', '#0000ff', '#00ffff', '#ffffff'] },
{ name: 'Infrared Rouge Red Jaune Yellow', colors: ['#000000', '#400000', '#c80000', '#ff8000', '#ffff00'] },
{ name: 'UV Scan Violet Purple Cyan', colors: ['#000010', '#080040', '#4000c0', '#a000ff', '#00ffff'] },
{ name: 'Heat Map Rouge Red Bleu Blue', colors: ['#0000aa', '#0088ff', '#00ffaa', '#ffff00', '#ff0000'] },
{ name: 'Audio Wave Vert Green Bleu Blue', colors: ['#001818', '#00808a', '#00d0d0', '#80f8f8', '#ffffff'] },
{ name: 'Chroma Key Vert Green', colors: ['#00a800', '#00c800', '#00e800', '#00ff00', '#80ff80'] },
{ name: 'Retroreflect Jaune Yellow Gris Gray', colors: ['#202020', '#808080', '#e8e880', '#f8f840', '#f8f800'] },
{ name: 'Overexposed Blanc White Jaune Yellow', colors: ['#f8f8d0', '#f8f8e0', '#f8f8f0', '#f8f8f8', '#ffffff'] },
{ name: 'Solarize Jaune Yellow Violet Purple', colors: ['#000000', '#400040', '#c000c0', '#f8a000', '#ffffff'] },
{ name: 'Double Exposure Bleu Blue Rose Pink', colors: ['#0a0028', '#280060', '#8040a8', '#d080c8', '#f8c0e0'] },
{ name: 'Color Bleed Rouge Red Bleu Blue', colors: ['#aa0000', '#cc4040', '#8040a0', '#4040cc', '#0000aa'] },
{ name: 'Film Grain Gris Gray Marron Brown', colors: ['#2a2018', '#484030', '#787060', '#a8a090', '#d0c8c0'] },
{ name: 'Burned Paper Marron Brown Noir Black', colors: ['#080400', '#281408', '#503020', '#807050', '#b0a080'] },
{ name: 'Lumen Jaune Yellow Blanc White', colors: ['#2a2000', '#888000', '#e8e000', '#f8f880', '#ffffff'] },
// =====================================================================
// NEW / NOUVEAUX — CULTURAL EXTRA (15)
// =====================================================================
{ name: 'Oaxaca Orange Marron Brown', colors: ['#3a0800', '#a03010', '#e07830', '#f0b870', '#f8e8c8'] },
{ name: 'Havana Marron Brown Jaune Yellow', colors: ['#2a1000', '#785018', '#c09030', '#e8c870', '#f8f0d0'] },
{ name: 'Casablanca Blanc White Bleu Blue', colors: ['#1a2848', '#2058a0', '#60a0d8', '#b0d8f0', '#f0f8ff'] },
{ name: 'Bali Vert Green Orange', colors: ['#1a2000', '#508030', '#c08820', '#f0a840', '#f8e8c0'] },
{ name: 'Nairobi Vert Green Marron Brown', colors: ['#1a1808', '#486020', '#80a848', '#b8c880', '#d8e0b0'] },
{ name: 'Petra Marron Brown Rouge Red', colors: ['#2a0808', '#8a2818', '#c06030', '#e09060', '#f0c8a0'] },
{ name: 'Reykjavik Bleu Blue Gris Gray', colors: ['#0a1828', '#203858', '#406090', '#7098c0', '#a8c8e0'] },
{ name: 'Hanoi Vert Green Jaune Yellow', colors: ['#182808', '#408830', '#78c050', '#c0e888', '#e8f8d0'] },
{ name: 'Tulum Bleu Blue Vert Green', colors: ['#001828', '#007878', '#30c0b0', '#80f0e0', '#d0fff8'] },
{ name: 'Isfahan Bleu Blue Jaune Yellow', colors: ['#0a1840', '#1a5080', '#4090d0', '#c0b040', '#f8e870'] },
{ name: 'Kathmandu Orange Marron Brown', colors: ['#2a1000', '#8a4018', '#d09030', '#f0c870', '#f8f0d0'] },
{ name: 'Lagos Rouge Red Orange', colors: ['#1a0000', '#700000', '#c84010', '#f07830', '#f8c880'] },
{ name: 'Lima Vert Green Jaune Yellow', colors: ['#182008', '#508030', '#98c040', '#d0e880', '#f0f8c0'] },
{ name: 'Tbilisi Violet Purple Marron Brown', colors: ['#2a0828', '#783050', '#c07080', '#e0a8a0', '#f8d8d0'] },
{ name: 'Muscat Jaune Yellow Orange', colors: ['#3a1808', '#a06028', '#e0a848', '#f8d880', '#f8f8e0'] },
];
const gradColors = f => {
if (f.noGrad) return f.tags.map(() => null);
const n = f.tags.length;
if (!n) return [];
const g = f.gradient || [f.colorStart || '#c0f0f8', f.colorEnd || '#183848'];
if (n === 1) return [g[0]];
return f.tags.map((_, i) => {
const p = (i / (n - 1)) * (g.length - 1);
const idx = Math.floor(p);
if (idx >= g.length - 1) return g[g.length - 1];
return lerp(g[idx], g[idx + 1], p - idx);
});
};
const gradChildColors = f => {
if (!f.gradFolders || f.noGrad) return (f.children || []).map(() => null);
const children = f.children || [];
const n = children.length;
if (!n) return [];
const g = f.gradient || [f.colorStart || '#c0f0f8', f.colorEnd || '#183848'];
if (n === 1) return [g[0]];
return children.map((_, i) => {
const p = (i / (n - 1)) * (g.length - 1);
const idx = Math.floor(p);
if (idx >= g.length - 1) return g[g.length - 1];
return lerp(g[idx], g[idx + 1], p - idx);
});
};
const findF = (id, l = S.folders) => {
for (const f of l) {
if (f.id === id) return f;
const r = findF(id, f.children || []);
if (r) return r;
}
return null;
};
const folderOf = tag => flat().find(f => (f.tags || []).includes(tag)) || null;
const allTagsOf = f => [...(f.tags || []), ...(f.children || []).flatMap(allTagsOf)];
const delF = (id, l = S.folders) => {
const i = l.findIndex(f => f.id === id);
if (i !== -1) { l.splice(i, 1); return true; }
return l.some(f => delF(id, f.children || []));
};
function extractFolder(id, l = S.folders) {
const i = l.findIndex(f => f.id === id);
if (i !== -1) return l.splice(i, 1)[0];
for (const f of l) {
const found = extractFolder(id, f.children || []);
if (found) return found;
}
return null;
}
function isDescendant(sourceId, targetId) {
const source = findF(sourceId);
if (!source) return false;
const check = children => children.some(c => c.id === targetId || check(c.children || []));
return check(source.children || []);
}
function getFolderPath(tagName) {
const path = [];
const search = (folders, ancestors) => {
for (const f of folders) {
const chain = [...ancestors, f];
if ((f.tags || []).includes(tagName)) { path.push(...chain); return true; }
if (search(f.children || [], chain)) return true;
}
return false;
};
search(S.folders, []);
return path;
}
const getNativeLis = () => Array.from(document.querySelectorAll('li.tag.has-button:not([data-mm])'));
const getMainUl = () => {
const candidates = Array.from(document.querySelectorAll('ul.tag-list:not([role="listbox"])'));
const withTags = candidates.find(ul => ul.querySelector('li.tag.has-button'));
if (withTags) return withTags;
const fromLi = document.querySelector('li.tag.has-button:not([data-mm])')?.closest('ul');
if (fromLi) return fromLi;
return candidates[0] || null;
};
const getLocOl = () => document.querySelector('section.location-preview ol.tag-list[role="listbox"]') || null;
function nameFromLi(li) {
const label = li.querySelector('label.tag_text, label');
if (!label) return '';
let n = '';
label.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) n += node.textContent; });
return n.trim() || (label.textContent || '').replace(/\s*\d+\s*$/, '').trim();
}
const countFromLi = li => li?.querySelector('small')?.textContent?.trim() || '';
let _nativeTagMapCache = null;
let _nativeTagMapTimer = null;
const liOfTag = name => {
if (!_nativeTagMapCache) {
_nativeTagMapCache = new Map();
document.querySelectorAll('li.tag.has-button:not([data-mm])').forEach(li => {
_nativeTagMapCache.set(nameFromLi(li), li);
});
if (!_nativeTagMapTimer) {
_nativeTagMapTimer = setTimeout(() => {
_nativeTagMapCache = null;
_nativeTagMapTimer = null;
}, 0);
}
}
return _nativeTagMapCache.get(name) || null;
};
function getTagColor(li) {
if (!li) return null;
if (li.dataset.mmColor) return li.dataset.mmColor;
const wasHidden = li.style.display === 'none';
if (wasHidden) li.style.removeProperty('display');
const bg = window.getComputedStyle(li).backgroundColor;
if (wasHidden) li.style.setProperty('display', 'none', 'important');
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
li.dataset.mmColor = bg;
return bg;
}
return null;
}
const STORAGE_PREFIX = '[⚠️_DO_NOT_DELETE_MM_CLOUD_SAVE]';
const OLD_STORAGE_PREFIX = '[⚠️_NE_PAS_SUPPRIMER_MM_CLOUD_SAVE]';
function getStorageTagLi() {
return getNativeLis().find(li => {
const n = nameFromLi(li);
return n.startsWith(STORAGE_PREFIX) || n.startsWith(OLD_STORAGE_PREFIX);
});
}
function showCloudSetupWizard() {
return new Promise(res => {
const ov = mk('div', '', 'position:fixed;inset:0;background:rgba(10,10,10,.7);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:999999;display:flex;align-items:center;justify-content:center;');
const box = mk('div', '', 'background:#1c1c1a;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:32px;width:420px;color:#e8e8e8;font-family:"Open Sans",sans-serif;box-shadow:0 24px 64px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.02) inset;text-align:left;position:relative;display:flex;flex-direction:column;gap:20px;');
const header = mk('div', '', 'display:flex;align-items:center;gap:12px;');
const iconWrap = mk('div', '', 'display:flex;align-items:center;justify-content:center;width:42px;height:42px;background:rgba(59,130,246,.15);border-radius:10px;color:#3b82f6;');
iconWrap.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"></path></svg>`;
const title = mk('h2', '', 'font-size:18px;font-weight:700;margin:0;color:#fff;letter-spacing:0.3px;');
title.textContent = 'Cloud Sync Setup';
header.append(iconWrap, title);
box.appendChild(header);
const p1 = mk('p', '', 'font-size:13.5px;color:rgba(255,255,255,.65);line-height:1.5;margin:0;');
p1.innerHTML = `To securely save your folders to the cloud, the script needs a native tag to store its data. <b>You only need to do this once per map.</b>`;
box.appendChild(p1);
const stepsWrap = mk('div', '', 'display:flex;flex-direction:column;gap:12px;');
const step1 = mk('div', '', 'display:flex;align-items:center;gap:14px;background:rgba(0,0,0,.25);padding:14px 16px;border-radius:12px;border:1px solid rgba(255,255,255,.04);');
step1.innerHTML = `<div style="background:#3b82f6;color:#fff;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:bold;flex-shrink:0;">1</div><div style="font-size:13.5px;line-height:1.4;color:#ddd;">Add a location anywhere on your map.</div>`;
const step2 = mk('div', '', 'display:flex;align-items:flex-start;gap:14px;background:rgba(0,0,0,.25);padding:14px 16px;border-radius:12px;border:1px solid rgba(255,255,255,.04);');
step2.innerHTML = `<div style="background:#3b82f6;color:#fff;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:bold;flex-shrink:0;margin-top:2px;">2</div><div style="font-size:13.5px;line-height:1.4;color:#ddd;width:100%;">Give it the exact tag name below:
<div style="display:flex;gap:8px;margin-top:12px;">
<input type="text" value="${STORAGE_PREFIX}" readonly style="flex:1;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#60a5fa;padding:10px 12px;font-family:monospace;font-size:11px;outline:none;" />
<button id="mm-copy-btn" style="background:#2d2d2a;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;padding:0 16px;cursor:pointer;font-weight:600;font-size:12px;transition:all .2s;">Copy</button>
</div>
</div>`;
const step3 = mk('div', '', 'display:flex;align-items:center;gap:14px;background:rgba(0,0,0,.25);padding:14px 16px;border-radius:12px;border:1px solid rgba(255,255,255,.04);');
step3.innerHTML = `<div style="background:#3b82f6;color:#fff;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:bold;flex-shrink:0;">3</div><div style="font-size:13.5px;line-height:1.4;color:#ddd;">Click <b>Save</b> at the bottom left of the panel.</div>`;
stepsWrap.append(step1, step2, step3);
box.appendChild(stepsWrap);
setTimeout(() => {
const copyBtn = box.querySelector('#mm-copy-btn');
copyBtn.onclick = () => {
navigator.clipboard.writeText(STORAGE_PREFIX);
copyBtn.textContent = 'Copied!';
copyBtn.style.background = '#22c55e';
copyBtn.style.color = '#fff';
copyBtn.style.borderColor = '#16a34a';
setTimeout(() => {
copyBtn.textContent = 'Copy';
copyBtn.style.background = '#2d2d2a';
copyBtn.style.color = '#fff';
copyBtn.style.borderColor = 'rgba(255,255,255,.1)';
}, 2000);
};
copyBtn.onmouseenter = () => { if(copyBtn.textContent === 'Copy') copyBtn.style.background = '#3f3f3a'; };
copyBtn.onmouseleave = () => { if(copyBtn.textContent === 'Copy') copyBtn.style.background = '#2d2d2a'; };
}, 0);
const btnRow = mk('div', '', 'display:flex;gap:12px;justify-content:center;margin-top:8px;');
const cancelBtn = mk('button', '', 'background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;color:rgba(255,255,255,.6);padding:10px 18px;cursor:pointer;font-size:13px;font-weight:600;transition:all 0.2s;');
cancelBtn.textContent = 'Cancel';
cancelBtn.onmouseenter = () => { cancelBtn.style.borderColor = 'rgba(255,255,255,.3)'; cancelBtn.style.color = '#fff'; };
cancelBtn.onmouseleave = () => { cancelBtn.style.borderColor = 'rgba(255,255,255,.15)'; cancelBtn.style.color = 'rgba(255,255,255,.6)'; };
const doneBtn = mk('button', '', 'background:#3b82f6;border:none;border-radius:8px;color:#fff;padding:10px 24px;cursor:pointer;font-size:13px;font-weight:700;transition:background 0.2s;');
doneBtn.textContent = 'I created the tag';
doneBtn.onmouseenter = () => doneBtn.style.background = '#2563eb';
doneBtn.onmouseleave = () => doneBtn.style.background = '#3b82f6';
cancelBtn.onclick = () => { ov.remove(); res(false); };
doneBtn.onclick = () => { ov.remove(); res(true); };
btnRow.append(cancelBtn, doneBtn);
box.appendChild(btnRow);
ov.appendChild(box);
document.body.appendChild(ov);
});
}
function showSaveWarningModal() {
return new Promise(res => {
const ov = mk('div', '', 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;');
const box = mk('div', '', 'background:rgb(37,37,33);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:24px;width:400px;color:#e8e8e8;font-family:"Open Sans",sans-serif;box-shadow:0 16px 48px rgba(0,0,0,.8);');
const ttl = mk('h2', '', 'margin:0 0 12px 0;font-size:16px;font-weight:700;color:#fff;');
ttl.textContent = 'Save Map & Folders';
const p = mk('p', '', 'margin:0 0 20px 0;font-size:13px;color:rgba(255,255,255,.7);line-height:1.5;');
p.textContent = 'Saving your folders requires saving the map. This will also save any recent locations or tags you added. Proceed?';
box.append(ttl, p);
const btnRow = mk('div', '', 'display:flex;gap:8px;justify-content:flex-end;');
const bCancel = mk('button', '', 'background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;color:rgba(255,255,255,.6);padding:8px 12px;cursor:pointer;font-size:12px;font-weight:600;transition:all 0.2s;');
bCancel.textContent = 'Cancel';
bCancel.onmouseenter = () => bCancel.style.borderColor = 'rgba(255,255,255,.3)';
bCancel.onmouseleave = () => bCancel.style.borderColor = 'rgba(255,255,255,.15)';
const bSkip = mk('button', '', 'background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:rgba(255,255,255,.8);padding:8px 12px;cursor:pointer;font-size:12px;font-weight:600;transition:all 0.2s;');
bSkip.textContent = "Understood, don't show again";
bSkip.onmouseenter = () => bSkip.style.background = 'rgba(255,255,255,.15)';
bSkip.onmouseleave = () => bSkip.style.background = 'rgba(255,255,255,.07)';
const bSave = mk('button', '', 'background:#3b82f6;border:none;border-radius:8px;color:#fff;padding:8px 16px;cursor:pointer;font-size:12px;font-weight:700;transition:background 0.2s;');
bSave.textContent = 'Save';
bSave.onmouseenter = () => bSave.style.background = '#2563eb';
bSave.onmouseleave = () => bSave.style.background = '#3b82f6';
bCancel.onclick = () => { ov.remove(); res('cancel'); };
bSkip.onclick = () => { ov.remove(); res('save_and_skip'); };
bSave.onclick = () => { ov.remove(); res('save'); };
btnRow.append(bCancel, bSkip, bSave);
box.appendChild(btnRow);
ov.appendChild(box);
document.body.appendChild(ov);
});
}
function injectFooterButtons() {
const footerContainer = document.querySelector('.map-meta__actions');
if (!footerContainer || document.getElementById('mm-footer-actions-wrapper')) return;
const wrapper = mk('span', '', 'display:inline-flex; align-items:center; margin-left:12px;');
wrapper.id = 'mm-footer-actions-wrapper';
const saveBtn = mk('button', 'button button--primary' + (isDirty ? ' mm-btn-dirty' : ''));
saveBtn.id = 'mm-folder-save-btn';
saveBtn.type = 'button';
saveBtn.title = 'Save folder tree to the Cloud';
saveBtn.innerHTML = `<span class="button__label">${isDirty ? 'Save Folders' : 'Folders Saved'}</span>`;
saveBtn.onclick = () => saveToCloud();
const loadBtn = mk('button', 'button button--primary');
loadBtn.id = 'mm-folder-load-btn';
loadBtn.type = 'button';
loadBtn.title = 'Restore folders from the Cloud (Undo local changes)';
loadBtn.innerHTML = '<span class="button__label">Load</span>';
loadBtn.onclick = () => loadFromCloud(true);
wrapper.append(saveBtn, loadBtn);
footerContainer.appendChild(wrapper);
}
async function saveToCloud() {
if (typeof LZString === 'undefined') { alert('Error: LZString library failed to load.'); return; }
let li = getStorageTagLi();
if (!li) {
const userDidSetup = await showCloudSetupWizard();
if (!userDidSetup) return;
await new Promise(r => setTimeout(r, 500));
li = getStorageTagLi();
if (!li) { alert('Storage tag not found. Make sure you created it with the exact name and clicked Save.'); return; }
}
const skipWarning = localStorage.getItem('mmapp_skip_save_warning') === 'true';
if (!skipWarning) {
const action = await showSaveWarningModal();
if (action === 'cancel') return;
if (action === 'save_and_skip') localStorage.setItem('mmapp_skip_save_warning', 'true');
}
const editBtn = li.querySelector('button.tag_button--edit, button[class*="edit"]');
if (!editBtn) return;
S.lastUpdate = Date.now();
const compressed = LZString.compressToEncodedURIComponent(JSON.stringify(S));
const currentName = nameFromLi(li);
const prefixToUse = currentName.startsWith(OLD_STORAGE_PREFIX) ? OLD_STORAGE_PREFIX : STORAGE_PREFIX;
const payload = `${prefixToUse}:::${compressed}`;
editBtn.click();
const nameInput = await waitDOM('div[role="dialog"] input[type="text"].input');
if (!nameInput) { console.warn("Save dialog didn't open in time."); return; }
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (nativeSetter) nativeSetter.call(nameInput, payload);
else nameInput.value = payload;
nameInput.dispatchEvent(new Event('input', { bubbles: true }));
nameInput.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(r => setTimeout(r, 100));
const dialog = nameInput.closest('div[role="dialog"]');
const btns = dialog?.querySelectorAll('.edit-tag-modal__actions button, button.save');
btns?.[btns.length - 1]?.click();
setDirty(false);
updateHideStyle();
await new Promise(r => setTimeout(r, 550));
const nativeSaveBtn = document.querySelector('button[data-qa="map-save"]');
if (nativeSaveBtn && !nativeSaveBtn.disabled) {
nativeSaveBtn.click();
} else {
console.warn("[mm-folders] Could not find the native save button using data-qa!");
}
const notif = mk('div', '', 'position:fixed;top:20px;right:20px;background:#3b82f6;color:#fff;padding:10px 20px;border-radius:8px;font-family:sans-serif;font-size:13px;z-index:999999;box-shadow:0 4px 12px rgba(0,0,0,.5);');
notif.textContent = "☁️ Folders and map saved!";
document.body.appendChild(notif);
setTimeout(() => notif.remove(), 3000);
}
function loadFromCloud(force = false) {
if (typeof LZString === 'undefined') { if (force) alert('Error: LZString library failed to load.'); return false; }
const li = getStorageTagLi();
if (!li) { if (force) alert(`No save found on this map.\n(No tag starting with ${STORAGE_PREFIX.substring(0, 10)}...)`); return false; }
const name = nameFromLi(li);
if (!name.includes(':::')) { if (force) alert('Storage tag exists but seems empty. Save your folders first.'); return false; }
try {
const dataPart = name.substring(name.indexOf(':::') + 3);
const decoded = LZString.decompressFromEncodedURIComponent(dataPart);
const parsed = JSON.parse(decoded || dataPart);
if (parsed?.folders) {
if (force || S.folders.length === 0 || (parsed.lastUpdate || 0) > S.lastUpdate) {
if (force && !confirm('⚠️ Warning: Forcing Load will overwrite your current local folders with the Cloud save. Continue?')) return false;
S = { folders: parsed.folders, lastUpdate: parsed.lastUpdate || Date.now() };
invalidateFlatCache();
saveS(false);
setDirty(false);
render();
return true;
}
}
} catch (e) { console.error('Cloud Load Error:', e); }
return false;
}
function injectCSS() {
if (document.getElementById('mm-css')) return;
const s = document.createElement('style');
s.id = 'mm-css';
s.textContent = `
li[data-mm-folder] { list-style:none; margin:0; padding:2px 0; width:100%; box-sizing:border-box; }
.mm-hd {
display:flex; align-items:center; gap:5px;
width:100%; height:32px; box-sizing:border-box;
padding:0 10px 0 0; border-radius:999px;
font-family:"Open Sans",sans-serif; font-size:16px; font-weight:400;
cursor:grab; user-select:none;
transition:filter .15s, box-shadow .15s;
}
.mm-hd:has(.mm-left-actions:hover, .mm-right-actions:hover) { cursor:default; }
.mm-hd:active:not(:has(.mm-left-actions:hover, .mm-right-actions:hover)) { cursor:grabbing; }
.mm-hd:hover { filter:brightness(1.08); }
.mm-hd--on { box-shadow:0 0 0 2px #fff!important; }
.mm-hd--off { opacity:.55; }
.mm-hd--partial { box-shadow:0 0 0 2px rgba(255,255,255,.4)!important; }
.mm-left-actions { display:flex; align-items:center; gap:2px; cursor:default; height:100%; padding-left:2px; }
.mm-right-actions { display:flex; align-items:center; gap:4px; cursor:default; height:100%; }
li[data-mm-folder].mm-drop-above { border-top:3px solid rgba(255,255,255,.8) !important; }
li[data-mm-folder].mm-drop-after { border-bottom:3px solid rgba(255,255,255,.8) !important; }
li[data-mm-folder].mm-drop-inside > .mm-hd { box-shadow:0 0 0 2px rgba(255,255,255,.9), 0 0 12px rgba(255,255,255,.3) !important; filter:brightness(1.15); }
.mm-arrow-zone {
display:flex; align-items:center; justify-content:center;
width:28px; height:28px; flex-shrink:0;
border-radius:50%; cursor:pointer; transition:background .12s;
}
.mm-arrow-zone:hover { background:rgba(0,0,0,.22); }
.mm-pencil {
display:flex; align-items:center; justify-content:center;
width:22px; height:22px; flex-shrink:0; cursor:pointer;
border-radius:50%; opacity:.55; transition:opacity .1s,background .1s;
}
.mm-pencil:hover { opacity:1; background:rgba(0,0,0,.18); }
.mm-body { display:flex; flex-wrap:wrap; gap:5px; padding:6px 8px 8px 16px; }
.mm-dropzone {
width:100%; padding:5px 12px; text-align:center;
border:1.5px dashed rgba(255,255,255,.2); border-radius:8px;
font-size:11px; font-family:"Open Sans",sans-serif;
color:rgba(255,255,255,.3); font-style:italic;
}
.mm-dh { outline:2px dashed rgba(255,255,255,.5)!important; outline-offset:2px!important; }
.mm-dropzone.mm-dh { border-color:rgba(255,255,255,.5)!important; color:rgba(255,255,255,.6)!important; outline:none!important; }
li.mm-sep { list-style:none; height:1px; background:rgba(120,120,220,.2); margin:6px 4px; }
li.mm-addli { list-style:none; margin:4px 0 5px; }
.mm-addbtn {
display:flex; align-items:center; justify-content:center; width:100%; height:32px;
padding:0 14px; border-radius:999px; border:none; cursor:pointer;
background:#E0E4E5; color:#121210;
font-size:16px; font-family:"Open Sans",sans-serif; font-weight:400;
box-sizing:border-box; transition:background .15s;
}
.mm-addbtn:hover { background:#d0d4d6; }
.mm-tag-btn {
display:inline-flex; align-items:center; gap:5px;
height:32px; padding:0 10px 0 8px; border-radius:999px; border:none;
font-family:"Open Sans",sans-serif; font-size:16px; font-weight:400;
cursor:pointer;
transition:filter .12s, box-shadow .12s, transform .12s;
white-space:nowrap;
}
.mm-tag-btn:hover { filter:brightness(1.12); transform:translateY(-1px); box-shadow:0 3px 8px rgba(0,0,0,.25); }
.mm-tag-btn--on { box-shadow:0 0 0 2px #fff!important; }
.mm-tag-drop-indicator { border-left: 3px solid #fff !important; }
.mm-subaddbtn {
background:rgba(255,255,255,.07); border:1.5px dashed rgba(255,255,255,.2);
border-radius:999px; color:rgba(255,255,255,.4);
font-size:11px; padding:2px 14px; height:24px;
cursor:pointer; font-family:"Open Sans",sans-serif; font-weight:600;
transition:color .12s,border-color .12s,background .12s;
display:flex; align-items:center; width:fit-content;
}
.mm-subaddbtn:hover { color:rgba(255,255,255,.8); border-color:rgba(255,255,255,.5); background:rgba(255,255,255,.12); }
.mm-sub-wrap { display:flex; flex-direction:column; gap:4px; padding:5px 0 3px 14px; }
.mm-grad { width:24px; height:7px; border-radius:4px; flex-shrink:0; opacity:.85; }
.mm-cnt { background:rgba(0,0,0,.2); border-radius:999px; padding:0 7px; font-size:11px; font-weight:700; flex-shrink:0; line-height:18px; }
.mm-actbtn { background:rgba(0,0,0,.18); border:none; cursor:pointer; border-radius:999px; padding:1px 7px; font-size:11px; font-weight:600; flex-shrink:0; opacity:.75; transition:opacity .1s; font-family:inherit; line-height:18px; display:inline-flex; align-items:center; justify-content:center; }
.mm-actbtn:hover { opacity:1; background:rgba(0,0,0,.32); }
#mm-folder-save-btn { background-color: #1e3a8a !important; border-color: #1e3a8a !important; color: #cbd5e1 !important; transition: all 0.2s; }
#mm-folder-save-btn:hover { background-color: #1e40af !important; border-color: #1e40af !important; color: #fff !important; }
#mm-folder-save-btn.mm-btn-dirty { background-color: #3b82f6 !important; border-color: #3b82f6 !important; color: #fff !important; }
#mm-folder-save-btn.mm-btn-dirty:hover { background-color: #2563eb !important; border-color: #2563eb !important; }
#mm-folder-load-btn { background-color: #7B68EE !important; border-color: #7B68EE !important; color: #fff !important; margin-left: 6px; }
#mm-folder-load-btn:hover { background-color: #6A5ACD !important; border-color: #6A5ACD !important; }
body:not(.mm-search-mode).mm-loading li.tag.has-button:not([data-mm]) { display: none !important; }
/* Cacher proprement les dossiers sans casser le menu ou les popups */
body.mm-search-mode ul.tag-list > li[data-mm="1"],
body.mm-search-mode li.mm-sep,
body.mm-search-mode li.mm-addli { display: none !important; }
body.mm-loc-open ul.tag-list > li[data-mm="1"],
body.mm-loc-open li.mm-sep,
body.mm-loc-open li.mm-addli { display: none !important; }
#mm-path-tooltip {
position: fixed; z-index: 999999; pointer-events: none; display: flex;
background: rgba(22, 22, 19, 0.78); border: 1px solid rgba(255,255,255,.08);
box-shadow: 0 4px 18px rgba(0,0,0,.4); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); font-family: "Open Sans", sans-serif;
font-size: 12.5px; opacity: 0; transition: opacity .18s ease, transform .18s ease; transform: translateY(3px);
}
#mm-path-tooltip.mm-tt-visible { opacity: 1; transform: translateY(0); }
#mm-path-tooltip:not(.mm-tt-col) { flex-direction: row; align-items: center; padding: 5px 13px; gap: 5px; border-radius: 999px; }
#mm-path-tooltip.mm-tt-col { flex-direction: column; align-items: flex-start; padding: 7px 12px 8px; gap: 3px; border-radius: 10px; }
#mm-path-tooltip .mm-tt-row { display: flex; align-items: center; gap: 5px; white-space: nowrap; line-height: 1; }
#mm-path-tooltip .mm-tt-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
#mm-path-tooltip .mm-tt-name { font-weight: 600; color: #dcdcdc; letter-spacing: .01em; }
#mm-path-tooltip .mm-tt-arrow { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 12px; height: 12px; opacity: .4; }
#mm-path-tooltip.mm-tt-col .mm-tt-connector { font-size: 10px; opacity: .28; line-height: 1; display: flex; align-items: center; user-select: none; }
`;
document.head.appendChild(s);
}
function updateHideStyle() {
const isSearch = document.body.classList.contains('mm-search-mode');
const assigned = getAssignedTags();
document.querySelectorAll('li:not([data-mm])').forEach(li => {
let hide = false;
const t = li.textContent || '';
if (t.includes(STORAGE_PREFIX) || t.includes(OLD_STORAGE_PREFIX)) {
hide = true;
} else if (!isSearch) {
const name = nameFromLi(li);
if (name && assigned.has(name)) hide = true;
}
if (hide) li.style.setProperty('display', 'none', 'important');
else li.style.removeProperty('display');
});
document.querySelectorAll('.mm-tag-btn').forEach(btn => {
const spans = btn.querySelectorAll('span');
let tagName = '';
spans.forEach(s => { if (!s.querySelector('svg')) tagName = s.textContent.trim(); });
if (!tagName) return;
const folder = folderOf(tagName);
if (folder && (folder.gradTags === false || folder.noGrad)) {
const li = liOfTag(tagName);
if (li) {
const bg = getTagColor(li) || '#5a5a8a';
btn.style.backgroundColor = bg;
btn.style.color = textOn(bg);
}
}
});
document.body.classList.remove('mm-loading');
document.body.classList.remove('mm-search-exiting');
}
function updateAllFolderVisuals() {
document.querySelectorAll('[data-mm-folder]').forEach(folderEl => {
const f = findF(folderEl.getAttribute('data-mm-folder'));
if (!f) return;
const hd = Array.from(folderEl.children).find(c => c.classList.contains('mm-hd'));
if (!hd) return;
const fullyOn = isFolderFullyOn(f);
const partialOn = !fullyOn && isFolderPartiallyOn(f);
hd.classList.remove('mm-hd--on', 'mm-hd--off', 'mm-hd--partial');
if (fullyOn) hd.classList.add('mm-hd--on');
else if (partialOn) hd.classList.add('mm-hd--partial');
else hd.classList.add('mm-hd--off');
});
document.querySelectorAll('.mm-tag-btn').forEach(btn => {
const spans = btn.querySelectorAll('span');
let t = '';
spans.forEach(s => { if (!s.querySelector('svg')) t = s.textContent.trim(); });
if (!t) return;
const nat = liOfTag(t);
if (!nat) return;
if (nat.classList.contains('is-selected')) btn.classList.add('mm-tag-btn--on');
else btn.classList.remove('mm-tag-btn--on');
});
}
function enterSearchMode() {
document.body.classList.add('mm-search-mode');
document.querySelectorAll('li:not([data-mm])').forEach(li => {
const t = li.textContent || '';
if (!t.includes(STORAGE_PREFIX) && !t.includes(OLD_STORAGE_PREFIX)) {
li.style.removeProperty('display');
}
});
}
function exitSearchMode() {
hidePathTooltip(true);
document.body.classList.remove('mm-search-mode');
updateHideStyle();
}
function watchSearchInput() {
const getInput = () => document.querySelector(
'body > div > div > div.tool-block.tag-manager > header > input'
);
let _boundInput = null;
let lastValue = '';
const attach = input => {
if (_boundInput === input) return;
_boundInput = input;
const onChange = () => {
const val = input.value || '';
if (val === lastValue) return;
lastValue = val;
if (val.length > 0) {
document.body.classList.remove('mm-search-exiting');
enterSearchMode();
} else {
document.body.classList.add('mm-search-exiting');
exitSearchMode();
}
};
input.addEventListener('input', onChange);
const obs = new MutationObserver(onChange);
obs.observe(input, { attributes: true, attributeFilter: ['value'] });
if (input.value) onChange();
};
setInterval(() => {
const input = getInput();
if (input && input !== _boundInput) attach(input);
}, 800);
}
let _tooltip = null;
let _tooltipTimeout = null;
let _tooltipLi = null;
function showPathTooltip(tagName, x, y) {
const path = getFolderPath(tagName);
if (!path.length) { hidePathTooltip(true); return; }
if (_tooltip && _tooltip.dataset.tag === tagName) return;
_tooltip?.remove();
_tooltip = null;
const tt = mk('div', '', '');
tt.id = 'mm-path-tooltip';
tt.dataset.tag = tagName;
const totalChars = path.reduce((sum, f) => sum + f.name.length, 0);
const isCol = totalChars > 35 || path.length > 3;
if (isCol) tt.classList.add('mm-tt-col');
path.forEach((folder, i) => {
const isLast = i === path.length - 1;
const row = mk('div', 'mm-tt-row');
if (isCol && i > 0) {
row.style.paddingLeft = (i * 9 + 2) + 'px';
row.style.borderLeft = `1.5px solid rgba(255,255,255,${Math.min(0.08 + i * 0.06, 0.24)})`;
row.style.marginLeft = '4px';
}
const bg = folder.color || '#c0f0f8';
const dot = mk('div', 'mm-tt-dot');
dot.style.background = bg;
dot.style.boxShadow = `0 0 5px ${bg}55`;
const name = mk('span', 'mm-tt-name');
name.textContent = folder.name;
row.appendChild(dot);
row.appendChild(name);
if (!isCol) {
tt.appendChild(row);
if (!isLast) {
const arr = mk('span', 'mm-tt-arrow');
arr.innerHTML = '<svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" style="color:rgba(255,255,255,.55)"><path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"/></svg>';
tt.appendChild(arr);
}
} else {
tt.appendChild(row);
if (!isLast) {
const connector = mk('div', 'mm-tt-connector');
connector.style.paddingLeft = (i * 9 + 6) + 'px';
connector.style.marginTop = '-1px';
connector.style.marginBottom = '-1px';
connector.innerHTML = '↳';
tt.appendChild(connector);
}
}
});
document.body.appendChild(tt);
_tooltip = tt;
const r = tt.getBoundingClientRect();
let left = x + 14;
let top = y - r.height / 2;
if (left + r.width > window.innerWidth - 8) left = x - r.width - 14;
if (top < 8) top = 8;
if (top + r.height > window.innerHeight - 8) top = window.innerHeight - r.height - 8;
tt.style.left = left + 'px';
tt.style.top = top + 'px';
requestAnimationFrame(() => { tt.classList.add('mm-tt-visible'); });
}
function hidePathTooltip(instant = false) {
clearTimeout(_tooltipTimeout);
_tooltipLi = null;
if (!_tooltip) return;
if (instant) { _tooltip.remove(); _tooltip = null; return; }
const el = _tooltip; _tooltip = null;
el.classList.remove('mm-tt-visible');
setTimeout(() => el.remove(), 220);
}
function bindTooltipListeners() {
document.addEventListener('mouseover', e => {
if (!document.body.classList.contains('mm-search-mode')) return;
const li = e.target.closest('li.tag.has-button:not([data-mm])');
if (!li) return;
if (li === _tooltipLi) return;
_tooltipLi = li;
clearTimeout(_tooltipTimeout);
const name = nameFromLi(li);
if (!name) return;
_tooltipTimeout = setTimeout(() => {
if (_tooltipLi !== li) return;
showPathTooltip(name, e.clientX, e.clientY);
}, 100);
}, true);
document.addEventListener('mouseout', e => {
const li = e.target.closest('li.tag.has-button:not([data-mm])');
if (!li) return;
if (li.contains(e.relatedTarget)) return;
if (li !== _tooltipLi) return;
_tooltipLi = null;
clearTimeout(_tooltipTimeout);
hidePathTooltip();
}, true);
}
function showInfoModal() {
const ov = mk('div', '', 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:999999;display:flex;align-items:center;justify-content:center;');
const box = mk('div', '', 'background:rgb(37,37,33);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:24px;width:420px;color:#e8e8e8;font-family:"Open Sans",sans-serif;box-shadow:0 16px 48px rgba(0,0,0,.8);line-height:1.5;font-size:13px;');
box.innerHTML = `
<h2 style="margin:0 0 15px 0;font-size:17px;color:#fff;">Help & Warnings</h2>
<ul style="padding-left:20px;margin-bottom:20px;color:rgba(255,255,255,.85);display:flex;flex-direction:column;gap:12px;">
<li><b>Security:</b> This script NEVER deletes your original tags. Deleting a folder simply releases the tags inside it back to the main list.</li>
<li><b>Renaming:</b> If you rename a tag via the site's native menu, it will drop out of its folder. Just drag it back in.</li>
<li><b>Cloud Save:</b> Remember to click the <b>Save Folders</b> button. The script creates a special tag [⚠️_DO_NOT_DELETE...] on your map to hide the data. Do not delete it!</li>
<li><b>Search:</b> When you type in the search bar, folders hide and all matching tags appear directly. Hover a tag to see which folder it belongs to.</li>
<li><b>Updates:</b> If the official website undergoes a major redesign, folder display might temporarily disappear (your tags will remain intact).</li>
</ul>
<div style="font-size:11px;opacity:.5;margin-bottom:20px;font-style:italic;">Note: This script is provided as is. The author is not responsible for the loss of your folder organization due to mishandling or unsaved cache clearing.</div>
<div style="text-align:right;"><button id="mm-close-info" style="background:#fff;border:none;border-radius:8px;color:#121210;padding:8px 18px;cursor:pointer;font-weight:700;font-size:12px;">Got it!</button></div>
`;
ov.appendChild(box);
document.body.appendChild(ov);
const closeBtn = box.querySelector('#mm-close-info');
closeBtn.onmouseenter = () => closeBtn.style.background = '#e0e0e0';
closeBtn.onmouseleave = () => closeBtn.style.background = '#fff';
closeBtn.onclick = () => ov.remove();
ov.onclick = e => { if (e.target === ov) ov.remove(); };
}
const SVG_PEN = `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" style="pointer-events:none;flex-shrink:0"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></svg>`;
const SVG_ARR = deg => `<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" style="display:block;transition:transform .2s;transform:rotate(${deg}deg);pointer-events:none"><path d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"/></svg>`;
const mk = (tag, cls = '', css = '', attrs = {}) => {
const e = document.createElement(tag);
if (cls) e.className = cls;
if (css) e.style.cssText = css;
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
return e;
};
let dragTag = null;
let dragFolderId = null;
function initDnD() {
document.addEventListener('dragstart', e => {
const li = e.target.closest('li.tag.has-button:not([data-mm])');
if (!li) return;
dragTag = nameFromLi(li);
try { e.dataTransfer.setData('text/plain', dragTag); } catch (_) {}
li.style.opacity = '.45';
}, true);
document.addEventListener('dragend', e => {
const li = e.target.closest('li.tag.has-button:not([data-mm])');
if (li) li.style.opacity = '';
if (li) dragTag = null;
}, true);
}
function dndOn(el, folderId) {
el.addEventListener('dragover', e => {
if (dragFolderId || !dragTag) return;
e.preventDefault(); e.stopPropagation(); el.classList.add('mm-dh');
});
el.addEventListener('dragleave', () => el.classList.remove('mm-dh'));
el.addEventListener('drop', e => {
if (dragFolderId) return;
e.preventDefault(); e.stopPropagation(); el.classList.remove('mm-dh');
const tag = dragTag || e.dataTransfer.getData('text/plain');
if (tag) { dragTag = null; moveTag(tag, folderId); }
});
}
function reRenderFolders() {
const ul = getMainUl();
if (ul) renderInto(ul);
updateHideStyle();
}
// --- REECRITURE EN PROFONDEUR : PURGE DU CACHE POUR moveTag ET removeTag ---
function moveTag(tag, folderId) {
const cleanTag = tag.trim();
let changed = false;
// Destruction DOM instantanée pour forcer la disparition visuelle sans attendre
document.querySelectorAll('.mm-tag-btn').forEach(btn => {
let btnTag = '';
btn.querySelectorAll('span').forEach(s => {
if (!s.querySelector('svg')) btnTag = s.textContent.trim();
});
if (btnTag === cleanTag) {
btn.remove();
changed = true;
}
});
const walkRemove = (list) => {
for (const f of list) {
if (f.tags) {
const originalLen = f.tags.length;
f.tags = f.tags.filter(t => t.trim() !== cleanTag);
if (f.tags.length !== originalLen) changed = true;
}
if (f.children) walkRemove(f.children);
}
};
walkRemove(S.folders);
const f = findF(folderId);
if (f && !f.tags.some(t => t.trim() === cleanTag)) {
f.tags.push(cleanTag);
changed = true;
}
if (changed) {
invalidateFlatCache();
saveS();
reRenderFolders();
}
}
function removeTag(tag) {
const cleanTag = tag.trim();
let changed = false;
// Destruction DOM instantanée pour forcer la disparition visuelle sans attendre
document.querySelectorAll('.mm-tag-btn').forEach(btn => {
let btnTag = '';
btn.querySelectorAll('span').forEach(s => {
if (!s.querySelector('svg')) btnTag = s.textContent.trim();
});
if (btnTag === cleanTag) {
btn.remove();
changed = true;
}
});
const walk = (list) => {
for (const f of list) {
if (f.tags) {
const originalLen = f.tags.length;
f.tags = f.tags.filter(t => t.trim() !== cleanTag);
if (f.tags.length !== originalLen) changed = true;
}
if (f.children) walk(f.children);
}
};
walk(S.folders);
if (changed) {
invalidateFlatCache();
saveS();
reRenderFolders();
}
}
function reorderFolder(dragId, targetId) {
if (dragId === targetId) return;
const dragged = extractFolder(dragId);
if (!dragged) return;
const insert = list => {
const i = list.findIndex(f => f.id === targetId);
if (i !== -1) { list.splice(i, 0, dragged); return true; }
return list.some(f => insert(f.children || []));
};
if (!insert(S.folders)) S.folders.push(dragged);
saveS(); reRenderFolders();
}
function moveFolderInto(dragId, targetId) {
if (dragId === targetId) return;
if (isDescendant(dragId, targetId)) return;
const dragged = extractFolder(dragId);
if (!dragged) return;
const target = findF(targetId);
if (!target) { S.folders.push(dragged); saveS(); reRenderFolders(); return; }
if (!target.children) target.children = [];
target.children.push(dragged);
target.expanded = true;
saveS(); reRenderFolders();
}
function reorderTag(draggedName, targetName) {
if (draggedName === targetName) return;
const targetFolder = folderOf(targetName);
if (!targetFolder) return;
const sourceFolder = folderOf(draggedName);
if (!sourceFolder || sourceFolder.id !== targetFolder.id) return;
targetFolder.tags = targetFolder.tags.filter(t => t !== draggedName);
const newTargetIndex = targetFolder.tags.indexOf(targetName);
targetFolder.tags.splice(newTargetIndex, 0, draggedName);
saveS(); reRenderFolders();
}
function modal(cfg) {
return new Promise(res => {
const ov = mk('div', '', 'position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:99999;display:flex;align-items:center;justify-content:center;', { 'data-mm': '1' });
const box = mk('div', '', 'background:rgb(37,37,33);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px 18px;width:300px;color:#e8e8e8;font-family:"Open Sans",sans-serif;font-size:13px;box-shadow:0 16px 48px rgba(0,0,0,.7);');
const ttl = mk('div', '', 'font-size:15px;font-weight:700;margin-bottom:16px;color:#fff;'); ttl.textContent = cfg.title; box.appendChild(ttl);
const fels = {};
const collect = () => {
const r = {};
Object.entries(fels).forEach(([k, e]) => {
if (e.type === 'checkbox') r[k] = e.checked;
else if (e.type === 'custom') r[k] = e.value;
else r[k] = e.value;
});
return r;
};
const confirm_ = () => { cleanup(); ov.remove(); res(collect()); };
const cancel_ = () => { cleanup(); ov.remove(); res(null); };
const onKeyDown = e => {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancel_(); }
if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') { e.preventDefault(); confirm_(); }
};
const cleanup = () => document.removeEventListener('keydown', onKeyDown, true);
document.addEventListener('keydown', onKeyDown, true);
(cfg.fields || []).forEach(f => {
const lbl = mk('label', '', 'display:block;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:rgba(255,255,255,.4);margin-bottom:6px;'); lbl.textContent = f.label; box.appendChild(lbl);
const inp = mk('input');
inp.type = 'text'; inp.placeholder = f.placeholder || ''; inp.value = f.value || '';
inp.style.cssText = 'background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;padding:9px 12px;font-size:13px;margin-bottom:14px;box-sizing:border-box;outline:none;font-family:"Open Sans",sans-serif;width:100%;display:block;';
inp.onfocus = () => inp.style.borderColor = 'rgba(255,255,255,.3)';
inp.onblur = () => inp.style.borderColor = 'rgba(255,255,255,.1)';
fels[f.key] = inp; box.appendChild(inp);
});
if (cfg.showFolderColor) {
const lbl = mk('label', '', 'display:block;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:rgba(255,255,255,.4);margin-bottom:6px;'); lbl.textContent = 'Folder color'; box.appendChild(lbl);
const fcRow = mk('div', '', 'display:flex;gap:8px;align-items:center;margin-bottom:6px;');
const inp = mk('input'); inp.type = 'color'; inp.value = cfg.folderColor || '#c0f0f8';
inp.style.cssText = 'width:48px;height:36px;border:1px solid rgba(255,255,255,.1);border-radius:8px;background:rgba(255,255,255,.07);cursor:pointer;padding:3px;flex-shrink:0;';
const fcHex = mk('input'); fcHex.type = 'text'; fcHex.value = inp.value; fcHex.maxLength = 7;
fcHex.style.cssText = 'flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;padding:8px 10px;font-size:12px;font-family:monospace;outline:none;min-width:0;cursor:text;';
fcHex.onfocus = () => fcHex.style.borderColor = 'rgba(255,255,255,.3)';
fcHex.onblur = () => fcHex.style.borderColor = 'rgba(255,255,255,.1)';
inp.oninput = () => { fcHex.value = inp.value; };
fcHex.oninput = () => { const v = fcHex.value.startsWith('#') ? fcHex.value : '#' + fcHex.value; if (/^#[0-9a-fA-F]{6}$/.test(v)) inp.value = v; };
fcRow.append(inp, fcHex);
fels.folderColor = inp; box.appendChild(fcRow);
let useParentColorState = !!cfg.useParentColor;
if (cfg.inheritedColor) {
const upcRow = mk('div', '', 'display:flex;align-items:center;gap:8px;margin-bottom:14px;');
const preview = mk('div', '', `width:18px;height:18px;border-radius:4px;background:${cfg.inheritedColor};flex-shrink:0;border:1px solid rgba(255,255,255,.2);`);
const upcBtn = mk('button', '', `display:flex;align-items:center;gap:6px;flex:1;background:${useParentColorState ? 'rgba(255,255,255,.1)' : 'transparent'};border:1px solid rgba(255,255,255,.12);border-radius:8px;color:${useParentColorState ? '#fff' : 'rgba(255,255,255,.4)'};padding:6px 10px;cursor:pointer;font-size:11px;font-family:"Open Sans",sans-serif;transition:all .15s;`);
const upcChk = mk('span', '', 'width:13px;height:13px;border:1.5px solid currentColor;border-radius:3px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;font-size:9px;');
upcChk.textContent = useParentColorState ? '✓' : '';
const upcTxt = mk('span'); upcTxt.textContent = 'Use parent color';
upcBtn.append(upcChk, upcTxt);
upcBtn.onclick = e => {
e.preventDefault(); e.stopPropagation();
useParentColorState = !useParentColorState;
upcChk.textContent = useParentColorState ? '✓' : '';
upcBtn.style.background = useParentColorState ? 'rgba(255,255,255,.1)' : 'transparent';
upcBtn.style.color = useParentColorState ? '#fff' : 'rgba(255,255,255,.4)';
fcRow.style.opacity = useParentColorState ? '.35' : '1';
fcRow.style.pointerEvents = useParentColorState ? 'none' : '';
};
if (useParentColorState) { fcRow.style.opacity = '.35'; fcRow.style.pointerEvents = 'none'; }
upcRow.append(preview, upcBtn);
box.appendChild(upcRow);
} else {
fcRow.style.marginBottom = '14px';
}
fels.useParentColor = { get checked() { return useParentColorState; }, type: 'checkbox' };
}
if (cfg.showColors) {
const lbl = mk('label', '', 'display:block;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:rgba(255,255,255,.4);margin-bottom:6px;');
lbl.textContent = 'Gradient Colors (2 to 6)';
box.appendChild(lbl);
const colorRow = mk('div', '', 'display:flex;gap:5px;margin-bottom:10px;align-items:center;');
let currentColors = [...(cfg.colors || ['#c0f0f8', '#183848'])];
if (currentColors.length < 2) currentColors.push('#183848');
const prev = mk('div', '', 'height:10px;border-radius:5px;margin-bottom:10px;opacity:.8;');
const upd = () => { prev.style.background = `linear-gradient(to right, ${currentColors.join(', ')})`; };
const allPresets = [
{ name: 'Volcanic Rouge Red', colors: ['#1a0000', '#8b1a00', '#ff4500', '#ffaa00'] },
...GRADIENT_PRESETS,
];
try {
const stored = JSON.parse(localStorage.getItem('mmapp_custom_presets') || '[]');
stored.forEach(p => { if (p.name && p.colors) allPresets.push(p); });
} catch (_) {}
const showAllGradientsModal = (presets, applyFn) => {
const ov2 = mk('div', '', 'position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:999999;display:flex;align-items:center;justify-content:center;');
const box2 = mk('div', '', 'background:rgb(37,37,33);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px;width:400px;height:500px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 16px 48px rgba(0,0,0,.8);');
const topRow = mk('div', '', 'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-shrink:0;');
const title = mk('div', '', 'color:#fff;font-weight:700;font-size:15px;font-family:"Open Sans",sans-serif;'); title.textContent = 'All Presets';
const closeBtn = mk('button', '', 'background:transparent;border:none;color:rgba(255,255,255,.5);cursor:pointer;font-size:16px;line-height:1;padding:0;transition:color .15s;'); closeBtn.innerHTML = '✕';
closeBtn.onmouseenter = () => closeBtn.style.color = '#fff';
closeBtn.onmouseleave = () => closeBtn.style.color = 'rgba(255,255,255,.5)';
closeBtn.onclick = () => ov2.remove();
topRow.append(title, closeBtn);
const searchInput = mk('input', '', 'background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:6px;color:#fff;padding:9px 12px;font-size:12px;margin-bottom:15px;outline:none;font-family:"Open Sans",sans-serif;');
searchInput.placeholder = 'Search "blue", "rose", "dark"...';
searchInput.onfocus = () => searchInput.style.borderColor = 'rgba(255,255,255,.3)';
searchInput.onblur = () => searchInput.style.borderColor = 'rgba(255,255,255,.1)';
const grid = mk('div', '', 'display:grid;grid-template-columns:repeat(5, 1fr);gap:6px;overflow-y:auto;padding:2px 4px;margin:0 -4px;flex:1;min-height:0;align-content:start;');
grid.style.scrollbarWidth = 'thin';
grid.style.scrollbarColor = 'rgba(255,255,255,.2) transparent';
const renderGrid = filterText => {
grid.innerHTML = '';
let term = filterText.toLowerCase();
const map = { 'bleu': 'blue', 'rouge': 'red', 'vert': 'green', 'jaune': 'yellow', 'noir': 'dark', 'blanc': 'white' };
Object.entries(map).forEach(([fr, en]) => { term = term.replace(fr, en); });
const filtered = presets.filter(p => p.name.toLowerCase().includes(term));
if (!filtered.length) {
const noRes = mk('div', '', 'grid-column:1/-1;color:rgba(255,255,255,.4);font-size:12px;text-align:center;padding:10px;');
noRes.textContent = 'No preset found.';
grid.appendChild(noRes);
return;
}
filtered.forEach(p => {
const chip = mk('div', '', `height:28px;border-radius:6px;cursor:pointer;background:linear-gradient(to right,${p.colors.join(',')});outline:1.5px solid transparent;outline-offset:1px;transition:outline-color .12s;`);
chip.title = p.name;
chip.onmouseenter = () => chip.style.outlineColor = 'rgba(255,255,255,.75)';
chip.onmouseleave = () => chip.style.outlineColor = 'transparent';
chip.onclick = () => { applyFn(p.colors, p); ov2.remove(); };
grid.appendChild(chip);
});
};
searchInput.oninput = e => renderGrid(e.target.value);
renderGrid('');
box2.append(topRow, searchInput, grid);
ov2.appendChild(box2);
ov2.onclick = e => { if (e.target === ov2) ov2.remove(); };
document.body.appendChild(ov2);
setTimeout(() => searchInput.focus(), 30);
};
let renderRecents = () => {};
const buildChip = (p, applyFn) => {
const chip = mk('div', '', `height:22px;border-radius:5px;cursor:pointer;background:linear-gradient(to right,${p.colors.join(',')});outline:1.5px solid transparent;outline-offset:1px;transition:outline-color .12s;box-sizing:border-box;`);
chip.title = p.name;
chip.onmouseenter = () => chip.style.outlineColor = 'rgba(255,255,255,.75)';
chip.onmouseleave = () => chip.style.outlineColor = 'transparent';
chip.onclick = e => {
e.preventDefault(); e.stopPropagation();
saveRecentPreset(p);
applyFn(p.colors);
renderRecents();
};
return chip;
};
const recentsWrap = mk('div', '', 'margin-bottom:10px;');
const recentsHeaderRow = mk('div', '', 'display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;');
const recentsLbl = mk('div', '', 'font-size:9px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:rgba(255,255,255,.3);');
recentsLbl.textContent = 'Recently Used';
recentsHeaderRow.appendChild(recentsLbl);
recentsWrap.appendChild(recentsHeaderRow);
const recentsGrid = mk('div', '', 'display:grid;grid-template-columns:repeat(4,1fr);gap:4px;');
recentsWrap.appendChild(recentsGrid);
renderRecents = () => {
if (!recentPresets.length) { recentsWrap.style.display = 'none'; return; }
recentsWrap.style.display = 'block';
recentsGrid.innerHTML = '';
recentPresets.slice(0, 4).forEach(p => recentsGrid.appendChild(buildChip(p, colors => {
currentColors = [...colors]; renderInputs(); upd();
})));
};
box.appendChild(recentsWrap);
renderRecents();
const presetsWrap = mk('div', '', 'margin-bottom:10px;');
const presetsHeaderRow = mk('div', '', 'display:flex;align-items:center;justify-content:space-between;margin-bottom:5px;');
const presetsLbl = mk('div', '', 'font-size:9px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:rgba(255,255,255,.3);');
presetsLbl.textContent = 'Presets';
const expandBtn = mk('button', '', 'background:transparent;border:none;color:rgba(255,255,255,.4);font-size:9px;cursor:pointer;padding:2px 6px;font-family:"Open Sans",sans-serif;letter-spacing:.5px;border-radius:4px;border:1px solid rgba(255,255,255,.15);transition:background .12s,color .12s,border-color .12s;');
expandBtn.textContent = `See all (${allPresets.length}) ↗`;
expandBtn.onmouseenter = () => { expandBtn.style.background = 'rgba(255,255,255,.08)'; expandBtn.style.color = '#fff'; expandBtn.style.borderColor = 'rgba(255,255,255,.3)'; };
expandBtn.onmouseleave = () => { expandBtn.style.background = 'transparent'; expandBtn.style.color = 'rgba(255,255,255,.4)'; expandBtn.style.borderColor = 'rgba(255,255,255,.15)'; };
presetsHeaderRow.append(presetsLbl, expandBtn);
presetsWrap.appendChild(presetsHeaderRow);
const presetsGrid = mk('div', '', 'display:grid;grid-template-columns:repeat(4,1fr);gap:4px;');
const PRESETS_VISIBLE = 16;
const renderPresets = () => {
presetsGrid.innerHTML = '';
allPresets.slice(0, PRESETS_VISIBLE).forEach(p => presetsGrid.appendChild(buildChip(p, colors => {
currentColors = [...colors]; renderInputs(); upd();
})));
expandBtn.textContent = `See all (${allPresets.length}) ↗`;
};
expandBtn.onclick = e => {
e.preventDefault(); e.stopPropagation();
showAllGradientsModal(allPresets, (colors, preset) => {
currentColors = [...colors];
if (preset) { saveRecentPreset(preset); renderRecents(); }
renderInputs(); upd();
});
};
presetsWrap.appendChild(presetsGrid);
const savePresetRow = mk('div', '', 'display:flex;gap:5px;align-items:center;margin-top:5px;');
const savePresetBtn = mk('button', '', 'background:rgba(255,255,255,.07);border:1px dashed rgba(255,255,255,.2);border-radius:5px;color:rgba(255,255,255,.4);font-size:10px;cursor:pointer;padding:3px 8px;font-family:"Open Sans",sans-serif;transition:all .12s;flex:1;');
savePresetBtn.textContent = '+ Save current as preset';
savePresetBtn.onmouseenter = () => { savePresetBtn.style.color = '#fff'; savePresetBtn.style.borderColor = 'rgba(255,255,255,.5)'; };
savePresetBtn.onmouseleave = () => { savePresetBtn.style.color = 'rgba(255,255,255,.4)'; savePresetBtn.style.borderColor = 'rgba(255,255,255,.2)'; };
savePresetBtn.onclick = e => {
e.preventDefault(); e.stopPropagation();
const name = prompt('Preset name?', 'My gradient');
if (!name) return;
const custom = { name, colors: [...currentColors], custom: true };
allPresets.push(custom);
try {
const stored = JSON.parse(localStorage.getItem('mmapp_custom_presets') || '[]');
stored.push(custom);
localStorage.setItem('mmapp_custom_presets', JSON.stringify(stored));
} catch (_) {}
saveRecentPreset(custom);
renderRecents();
renderPresets();
};
savePresetRow.appendChild(savePresetBtn);
presetsWrap.appendChild(savePresetRow);
renderPresets();
box.append(presetsWrap);
const renderInputs = () => {
colorRow.innerHTML = '';
currentColors.forEach((c, idx) => {
const colWrap = mk('div', '', 'display:flex;flex-direction:column;gap:3px;flex:1;min-width:0;');
const inp = mk('input'); inp.type = 'color'; inp.value = c;
inp.style.cssText = 'width:100%;height:28px;border:1px solid rgba(255,255,255,.1);border-radius:6px;background:rgba(255,255,255,.07);cursor:pointer;padding:1px;';
const hexEl = mk('input'); hexEl.type = 'text'; hexEl.value = c; hexEl.maxLength = 7;
hexEl.style.cssText = 'width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:5px;color:rgba(255,255,255,.7);padding:3px 5px;font-size:10px;font-family:monospace;outline:none;box-sizing:border-box;';
hexEl.onfocus = () => hexEl.style.borderColor = 'rgba(255,255,255,.3)';
hexEl.onblur = () => hexEl.style.borderColor = 'rgba(255,255,255,.08)';
inp.oninput = () => { currentColors[idx] = inp.value; hexEl.value = inp.value; upd(); };
hexEl.oninput = () => { const v = hexEl.value.startsWith('#') ? hexEl.value : '#' + hexEl.value; if (/^#[0-9a-fA-F]{6}$/.test(v)) { inp.value = v; currentColors[idx] = v; upd(); } };
colWrap.append(inp, hexEl);
colorRow.appendChild(colWrap);
});
const controls = mk('div', '', 'display:flex;flex-direction:column;gap:2px;margin-left:5px;');
const btnAdd = mk('button', '', 'background:rgba(255,255,255,.1);border:none;border-radius:4px;color:#fff;width:20px;height:15px;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;');
btnAdd.textContent = '+';
btnAdd.onclick = e => { e.preventDefault(); if (currentColors.length < 6) { currentColors.push(currentColors[currentColors.length - 1]); renderInputs(); upd(); } };
const btnRem = mk('button', '', 'background:rgba(255,255,255,.1);border:none;border-radius:4px;color:#fff;width:20px;height:15px;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;');
btnRem.textContent = '-';
btnRem.onclick = e => { e.preventDefault(); if (currentColors.length > 2) { currentColors.pop(); renderInputs(); upd(); } };
const btnInv = mk('button', '', 'background:rgba(255,255,255,.1);border:none;border-radius:4px;color:#fff;width:20px;height:15px;font-size:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin-top:2px;');
btnInv.innerHTML = '⇄'; btnInv.title = 'Invert gradient';
btnInv.onclick = e => { e.preventDefault(); currentColors.reverse(); renderInputs(); upd(); };
controls.append(btnAdd, btnRem, btnInv);
colorRow.appendChild(controls);
};
renderInputs();
upd();
box.append(colorRow, prev);
let gradTagsState = cfg.gradTags !== false && !cfg.noGrad;
let gradFoldersState = !!cfg.gradFolders && !cfg.noGrad;
const mkToggleBtn = (label, active) => {
const btn = mk('button', '', `display:flex;align-items:center;gap:8px;width:100%;background:${active ? 'rgba(255,255,255,.1)' : 'transparent'};border:1px solid rgba(255,255,255,.12);border-radius:8px;color:${active ? '#fff' : 'rgba(255,255,255,.4)'};padding:7px 12px;cursor:pointer;font-size:11px;font-family:"Open Sans",sans-serif;margin-bottom:6px;transition:all .15s;text-align:left;`);
const chk = mk('span', '', 'width:14px;height:14px;border:1.5px solid currentColor;border-radius:3px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;font-size:10px;');
chk.textContent = active ? '✓' : '';
const txt = mk('span'); txt.textContent = label;
btn.append(chk, txt);
btn._isActive = active;
btn._chk = chk;
btn._toggle = () => {
btn._isActive = !btn._isActive;
chk.textContent = btn._isActive ? '✓' : '';
btn.style.background = btn._isActive ? 'rgba(255,255,255,.1)' : 'transparent';
btn.style.color = btn._isActive ? '#fff' : 'rgba(255,255,255,.4)';
};
return btn;
};
const btnGradTags = mkToggleBtn('Apply gradient to tags', gradTagsState);
const btnGradFolders = mkToggleBtn('Apply gradient to sub-folders', gradFoldersState);
const syncStates = () => {
const noGrad = !btnGradTags._isActive && !btnGradFolders._isActive;
colorRow.style.opacity = noGrad ? '.35' : '1';
colorRow.style.pointerEvents = noGrad ? 'none' : '';
prev.style.opacity = noGrad ? '.35' : '.8';
};
btnGradTags.onclick = e => { e.preventDefault(); e.stopPropagation(); btnGradTags._toggle(); gradTagsState = btnGradTags._isActive; syncStates(); };
btnGradFolders.onclick = e => { e.preventDefault(); e.stopPropagation(); btnGradFolders._toggle(); gradFoldersState = btnGradFolders._isActive; syncStates(); };
syncStates();
box.append(btnGradTags, btnGradFolders);
fels.noGrad = { get checked() { return !btnGradTags._isActive && !btnGradFolders._isActive; }, type: 'checkbox' };
fels.gradTags = { get checked() { return gradTagsState; }, type: 'checkbox' };
fels.gradFolders = { get checked() { return gradFoldersState; }, type: 'checkbox' };
fels.gradientColors = { get value() { return currentColors; }, type: 'custom' };
}
const acts = mk('div', '', 'display:flex;gap:8px;justify-content:flex-end;margin-top:4px;');
const bC = mk('button', '', 'background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;color:rgba(255,255,255,.5);padding:8px 18px;cursor:pointer;font-size:12px;font-family:"Open Sans",sans-serif;'); bC.textContent = 'Cancel';
const bO = mk('button', '', 'background:#fff;border:none;border-radius:8px;color:#252521;padding:8px 18px;cursor:pointer;font-size:12px;font-weight:700;font-family:"Open Sans",sans-serif;'); bO.textContent = cfg.okText || 'OK';
bC.onmouseenter = () => bC.style.borderColor = 'rgba(255,255,255,.35)'; bC.onmouseleave = () => bC.style.borderColor = 'rgba(255,255,255,.15)';
bO.onmouseenter = () => bO.style.background = 'rgba(255,255,255,.85)'; bO.onmouseleave = () => bO.style.background = '#fff';
acts.append(bC, bO); box.appendChild(acts); ov.appendChild(box); document.body.appendChild(ov);
bO.onclick = confirm_;
bC.onclick = cancel_;
ov.onclick = e => { if (e.target === ov) cancel_(); };
setTimeout(() => box.querySelector('input[type=text]')?.focus(), 30);
});
}
let _ctx = null;
const closeCtx = () => { _ctx?.remove(); _ctx = null; };
function showCtx(x, y, items) {
closeCtx();
const m = mk('div', '', `position:fixed;left:${x}px;top:${y}px;background:rgba(22,22,19,0.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:6px;z-index:100000;min-width:240px;max-height:45vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:"Open Sans",sans-serif;font-size:13px;color:#e8e8e8;`, { 'data-mm': '1' });
m.style.scrollbarWidth = 'thin';
m.style.scrollbarColor = 'rgba(255,255,255,.2) transparent';
_ctx = m;
items.forEach(item => {
if (item === '---') { const s = mk('div', '', 'height:1px;background:rgba(255,255,255,.08);margin:4px 0;'); m.appendChild(s); return; }
const d = mk('div', '', 'padding:6px 10px;cursor:pointer;border-radius:6px;display:flex;align-items:center;gap:8px;position:relative;transition:background 0.1s;');
d.onmouseenter = () => d.style.background = 'rgba(255,255,255,.08)';
d.onmouseleave = () => d.style.background = 'transparent';
if (item.html) {
d.innerHTML = item.html;
} else if (item.sub?.length) {
d.innerHTML = `<span style="min-width:16px;opacity:.6">${item.icon || ''}</span><span style="flex:1">${item.label}</span><span style="opacity:.3;font-size:9px">▶</span>`;
const sub = mk('div', '', `display:none;position:absolute;left:calc(100% + 4px);top:-4px;background:rgba(22,22,19,0.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:6px;min-width:180px;max-height:300px;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.5);z-index:100001;font-family:"Open Sans",sans-serif;font-size:13px;`);
item.sub.forEach(s => {
const si = mk('div', '', 'padding:6px 10px;cursor:pointer;border-radius:6px;display:flex;align-items:center;gap:8px;white-space:nowrap;color:#e8e8e8;');
si.onmouseenter = () => si.style.background = 'rgba(255,255,255,.08)';
si.onmouseleave = () => si.style.background = 'transparent';
si.innerHTML = `<span style="min-width:16px;opacity:.6">${s.icon || ''}</span><span>${s.label}</span>`;
si.onclick = e => { e.stopPropagation(); closeCtx(); s.action?.(); };
sub.appendChild(si);
});
d.appendChild(sub);
d.onmouseenter = () => { d.style.background = 'rgba(255,255,255,.08)'; sub.style.display = 'block'; };
d.onmouseleave = () => { d.style.background = 'transparent'; sub.style.display = 'none'; };
} else {
d.innerHTML = `<span style="min-width:16px;opacity:.6">${item.icon || ''}</span><span>${item.label}</span>`;
}
d.onclick = e => { e.stopPropagation(); closeCtx(); item.action?.(); };
m.appendChild(d);
});
document.body.appendChild(m);
requestAnimationFrame(() => {
const r = m.getBoundingClientRect();
if (r.right > window.innerWidth) m.style.left = (x - r.width - 8) + 'px';
if (r.bottom > window.innerHeight) m.style.top = (y - r.height - 8) + 'px';
});
setTimeout(() => document.addEventListener('click', closeCtx, { once: true }), 0);
}
function clickNative(tagName) {
const li = liOfTag(tagName);
if (!li) return;
(li.querySelector('label.tag_text, label') || li).click();
}
async function setNativeTagColor(tagName, hexColor, finalDelay = 100) {
const li = liOfTag(tagName);
if (!li) return false;
const editBtn = li.querySelector('button.tag_button--edit, button[class*="edit"]');
if (!editBtn) return false;
editBtn.click();
const hexInput = await waitDOM('div[role="dialog"] input.input.hex-color, div[role="dialog"] input[class*="hex-color"]');
if (!hexInput) {
console.warn(`[mm-folders] setNativeTagColor: hex input not found for tag "${tagName}"`);
const dialog = document.querySelector('div[role="dialog"]');
dialog?.querySelectorAll('button')[0]?.click();
return false;
}
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
const hex = hexColor.replace('#', '');
if (nativeSetter) nativeSetter.call(hexInput, hex);
else hexInput.value = hex;
hexInput.dispatchEvent(new Event('input', { bubbles: true }));
hexInput.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(r => setTimeout(r, 80));
const dialog = hexInput.closest('div[role="dialog"]');
const btns = dialog?.querySelectorAll('.edit-tag-modal__actions button, button.save');
btns?.[btns.length - 1]?.click();
await new Promise(r => setTimeout(r, finalDelay));
return true;
}
async function applyFolderColorsToNative(folder) {
let failed = 0;
if (folder.gradTags !== false && !folder.noGrad) {
const colors = gradColors(folder);
for (let i = 0; i < folder.tags.length; i++) {
const ok = await setNativeTagColor(folder.tags[i], rgbToHex(colors[i] || folder.colorStart || '#c0f0f8'), 120);
if (!ok) failed++;
}
}
for (const child of folder.children || []) await applyFolderColorsToNative(child);
return failed;
}
async function applyTagToLoc(tagName) {
const searchInput = document.querySelector(
'section.location-preview input[type="text"], .location-preview-tags input, input[placeholder*="tag" i], input[placeholder*="Add"]'
);
if (!searchInput) return false;
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
const setVal = v => {
if (nativeSetter) nativeSetter.call(searchInput, v);
else searchInput.value = v;
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
searchInput.dispatchEvent(new Event('change', { bubbles: true }));
};
searchInput.focus();
setVal(tagName);
return new Promise(resolve => {
let tries = 0;
const interval = setInterval(() => {
tries++;
const ol = getLocOl();
if (ol) {
for (const li of ol.querySelectorAll('li:not([data-mm])')) {
const span = li.querySelector('span.tag_text, span[class*="tag_text"]');
const name = (span?.textContent || '').trim();
if (name !== tagName) continue;
const btn = li.querySelector('button[class*="add"]') || li.querySelector('button');
(btn || li).click();
clearInterval(interval);
setTimeout(() => { setVal(''); searchInput.blur(); }, 80);
resolve(true);
return;
}
}
if (tries > 20) { clearInterval(interval); resolve(false); }
}, 60);
});
}
async function newFolder(parentId = null) {
const parent = parentId ? findF(parentId) : null;
const r = await modal({
title: parentId ? 'New subfolder' : 'New folder',
fields: [{ key: 'name', label: 'Name', type: 'text', placeholder: 'e.g. Guardrails' }],
showFolderColor: true, folderColor: parent?.color || '#c0f0f8',
showColors: true, colors: parent?.gradient || ['#c0f0f8', '#183848'],
noGrad: parent?.noGrad || false,
gradTags: parent?.gradTags !== false,
gradFolders: !!parent?.gradFolders,
okText: 'Create'
});
if (!r?.name?.trim()) return;
const gradientColors = r.gradientColors || ['#c0f0f8', '#183848'];
const f = {
id: uid(), name: r.name.trim(),
color: r.folderColor || '#c0f0f8',
gradient: gradientColors, colorStart: gradientColors[0], colorEnd: gradientColors[gradientColors.length - 1],
noGrad: !!r.noGrad, gradTags: r.gradTags !== false, gradFolders: !!r.gradFolders,
expanded: true, visible: true, tags: [], children: []
};
if (parentId) { const p = findF(parentId); if (p) { p.children.push(f); saveS(); reRenderFolders(); return; } }
S.folders.push(f); saveS(); reRenderFolders();
}
async function editFolder(id) {
const f = findF(id); if (!f) return;
const r = await modal({
title: 'Edit folder',
fields: [{ key: 'name', label: 'Name', type: 'text', value: f.name }],
showFolderColor: true, folderColor: f.color || '#c0f0f8',
inheritedColor: f._inheritedColor || null,
useParentColor: !!f.useParentColor,
showColors: true, colors: f.gradient || ['#c0f0f8', '#183848'],
noGrad: f.noGrad || false, gradTags: f.gradTags !== false, gradFolders: !!f.gradFolders,
okText: 'Save'
});
if (!r?.name?.trim()) return;
const gradientColors = r.gradientColors || ['#c0f0f8', '#183848'];
f.name = r.name.trim(); f.color = r.folderColor || f.color;
f.gradient = gradientColors; f.colorStart = gradientColors[0]; f.colorEnd = gradientColors[gradientColors.length - 1];
f.noGrad = !!r.noGrad; f.gradTags = r.gradTags !== false; f.gradFolders = !!r.gradFolders;
f.useParentColor = !!r.useParentColor;
saveS(); reRenderFolders();
}
function isFolderFullyOn(folder) {
const tags = allTagsOf(folder).filter(tag => !!liOfTag(tag));
if (!tags.length) return false;
return tags.every(tag => liOfTag(tag)?.classList.contains('is-selected'));
}
function isFolderPartiallyOn(folder) {
return allTagsOf(folder).some(tag => liOfTag(tag)?.classList.contains('is-selected'));
}
function toggleVis(folder) {
const targetState = !isFolderFullyOn(folder);
const tagsToToggle = allTagsOf(folder).filter(tag => {
const li = liOfTag(tag);
if (!li) return false;
return targetState !== li.classList.contains('is-selected');
});
if (!tagsToToggle.length) return;
// 1. Mise à jour visuelle immédiate du folder header
const folderEl = document.querySelector(`[data-mm-folder="${folder.id}"]`);
const hd = folderEl ? Array.from(folderEl.children).find(c => c.classList.contains('mm-hd')) : null;
if (hd) {
hd.classList.remove('mm-hd--on', 'mm-hd--off', 'mm-hd--partial');
hd.classList.add(targetState ? 'mm-hd--on' : 'mm-hd--off');
}
// 2. Mise à jour visuelle immédiate des tag buttons
tagsToToggle.forEach(tag => {
const btn = Array.from(document.querySelectorAll('.mm-tag-btn')).find(b => {
let t = '';
b.querySelectorAll('span').forEach(s => { if (!s.querySelector('svg')) t = s.textContent.trim(); });
return t === tag;
});
if (btn) {
if (targetState) btn.classList.add('mm-tag-btn--on');
else btn.classList.remove('mm-tag-btn--on');
}
});
// 3. Clics natifs en batch asynchrone par paquets de 5
folder.visible = targetState;
saveS(false);
const BATCH_SIZE = 25;
const BATCH_DELAY = 1; // ms entre chaque batch
const runBatch = (index) => {
const batch = tagsToToggle.slice(index, index + BATCH_SIZE);
batch.forEach(tag => clickNative(tag));
if (index + BATCH_SIZE < tagsToToggle.length) {
setTimeout(() => runBatch(index + BATCH_SIZE), BATCH_DELAY);
} else {
// Sync final après tous les clics
setTimeout(updateAllFolderVisuals, 60);
}
};
runBatch(0);
}
// ─── TAG BUTTON ──────────────────────────────────────────────────────────────
function buildTagBtn(tag, color) {
const li = liOfTag(tag);
const count = countFromLi(li);
const bg = color || getTagColor(li) || '#5a5a8a';
const fg = textOn(bg);
const isOn = li?.classList.contains('is-selected') ?? false;
const btn = mk('button', 'mm-tag-btn' + (isOn ? ' mm-tag-btn--on' : ''), '', { 'data-mm': '1', 'draggable': 'true' });
btn.style.backgroundColor = bg;
btn.style.color = fg;
btn.addEventListener('dragover', e => {
if (dragTag && dragTag !== tag) { e.preventDefault(); e.stopPropagation(); btn.classList.add('mm-tag-drop-indicator'); }
});
btn.addEventListener('dragleave', () => btn.classList.remove('mm-tag-drop-indicator'));
btn.addEventListener('drop', e => {
if (dragTag && dragTag !== tag) {
e.preventDefault(); e.stopPropagation();
btn.classList.remove('mm-tag-drop-indicator');
reorderTag(dragTag, tag); dragTag = null;
}
});
const pen = mk('span', '', 'opacity:.7;flex-shrink:0;display:inline-flex;align-items:center;cursor:pointer;');
pen.innerHTML = SVG_PEN;
pen.addEventListener('click', e => {
e.stopPropagation(); e.preventDefault();
const nat = liOfTag(tag);
if (!nat) return;
nat.querySelector('button.tag_button--edit, button[class*="edit"]')?.click();
});
btn.appendChild(pen);
const nameSp = mk('span', '', 'pointer-events:none;');
nameSp.textContent = tag;
btn.appendChild(nameSp);
if (count) {
const cSp = mk('small', '', 'margin-left:0.375rem;font-weight:600;vertical-align:middle;pointer-events:none;');
cSp.textContent = count;
btn.appendChild(cSp);
}
btn.onclick = async e => {
if (e.target === pen || pen.contains(e.target)) return;
e.stopPropagation();
if (getLocOl()) {
await applyTagToLoc(tag);
} else {
clickNative(tag);
setTimeout(updateAllFolderVisuals, 60);
}
};
btn.addEventListener('dragstart', e => {
if (e.target === pen || pen.contains(e.target)) { e.preventDefault(); return; }
dragTag = tag;
try { e.dataTransfer.setData('text/plain', tag); } catch (_) {}
btn.style.opacity = '.45';
});
btn.addEventListener('dragend', () => { btn.style.opacity = ''; dragTag = null; });
btn.addEventListener('contextmenu', e => {
e.preventDefault();
e.stopPropagation();
const natLi = liOfTag(tag);
if (!natLi) return;
const observer = new MutationObserver((mutations, obs) => {
const nativeMenu = document.querySelector('[data-side="right"][role="menu"].context-menu, [role="menu"].context-menu');
if (nativeMenu) {
if (!nativeMenu.querySelector('[data-mm-injected]')) {
injectIntoNativeMenu(tag, nativeMenu);
}
obs.disconnect();
natLi.style.removeProperty('position');
natLi.style.removeProperty('left');
natLi.style.removeProperty('top');
natLi.style.removeProperty('opacity');
natLi.style.removeProperty('pointer-events');
natLi.style.removeProperty('z-index');
natLi.style.setProperty('display', 'none', 'important');
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 500);
natLi.style.removeProperty('display');
natLi.style.setProperty('position', 'fixed', 'important');
natLi.style.setProperty('left', e.clientX + 'px', 'important');
natLi.style.setProperty('top', e.clientY + 'px', 'important');
natLi.style.setProperty('opacity', '0', 'important');
natLi.style.setProperty('pointer-events', 'auto', 'important');
natLi.style.setProperty('z-index', '-1', 'important');
natLi.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true, cancelable: true,
clientX: e.clientX, clientY: e.clientY, button: 2
}));
});
return btn;
}
const escapeHTML = (str) => {
return str.replace(/[&<>'"]/g,
tag => ({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag] || tag)
);
};
// ─── INJECTION DANS LE MENU NATIF ────────────────────────────────────────────
function injectIntoNativeMenu(tag, nativeMenu) {
const cur = folderOf(tag);
const allF = flat();
// Ligne séparatrice
const sep = document.createElement('div');
sep.setAttribute('data-mm-injected', '1');
sep.style.cssText = 'height:1px;background:rgba(255,255,255,.1);margin:3px 0;';
nativeMenu.appendChild(sep);
if (allF.length) {
const moveItem = document.createElement('div');
moveItem.setAttribute('data-mm-injected', '1');
moveItem.setAttribute('role', 'menuitem');
moveItem.setAttribute('tabindex', '-1');
moveItem.className = 'context-menu__item';
moveItem.textContent = ' Move to folder...';
moveItem.onclick = e => {
e.stopPropagation();
const rect = moveItem.getBoundingClientRect();
document.body.click();
const getDepth = (id, list = S.folders, currentDepth = 0) => {
for (const f of list) {
if (f.id === id) return currentDepth;
const d = getDepth(id, f.children || [], currentDepth + 1);
if (d !== -1) return d;
}
return -1;
};
const cur2 = folderOf(tag);
// Construction du menu façon tooltip
setTimeout(() => {
showCtx(rect.right + 4, rect.top, flat().map(f => {
const depth = getDepth(f.id);
const bg = f.color || '#c0f0f8';
const isCurrent = f.id === cur2?.id;
const pad = depth * 14;
// Création du connecteur "L" pour les sous dossiers
let connector = '';
if (depth > 0) {
connector = `
<div style="position:absolute;left:${pad - 8}px;top:0;height:50%;border-left:1.5px solid rgba(255,255,255,0.15);border-bottom:1.5px solid rgba(255,255,255,0.15);width:6px;border-bottom-left-radius:3px;"></div>
`;
}
const html = `
<div style="display:flex;align-items:center;gap:6px;padding-left:${pad}px;position:relative;width:100%;">
${connector}
<div style="width:8px;height:8px;border-radius:50%;background:${bg};box-shadow:0 0 5px ${bg}55;flex-shrink:0;"></div>
<span style="font-weight:${isCurrent ? '700' : '600'};color:${isCurrent ? '#fff' : '#dcdcdc'};flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:0.01em;">${escapeHTML(f.name)}</span>
${isCurrent ? '<span style="color:rgba(255,255,255,0.5);font-size:11px;font-weight:400;margin-left:4px;">✓</span>' : ''}
</div>
`;
return {
html,
action: () => moveTag(tag, f.id)
};
}));
}, 50);
};
nativeMenu.appendChild(moveItem);
}
if (cur) {
const removeItem = document.createElement('div');
removeItem.setAttribute('data-mm-injected', '1');
removeItem.setAttribute('role', 'menuitem');
removeItem.setAttribute('tabindex', '-1');
removeItem.className = 'context-menu__item';
removeItem.textContent = `Remove from "${cur.name}"`;
removeItem.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
document.body.click();
setTimeout(() => removeTag(tag), 150);
};
nativeMenu.appendChild(removeItem);
}
const nativeDelete = Array.from(nativeMenu.querySelectorAll('.context-menu__item')).find(el => el.textContent.toLowerCase().includes('delete') || el.querySelector('.icon-trash') || el.textContent.includes('Supprimer'));
if (nativeDelete) {
nativeDelete.addEventListener('click', () => {
setTimeout(() => removeTag(tag), 150);
});
}
}
// ─── CONTEXT MENU SUR TAGS NATIFS (mode recherche) ───────────────────────────
function bindCtxMenu() {
document.addEventListener('contextmenu', e => {
if (e.target.closest('[data-mm]')) return;
const li = e.target.closest('li.tag.has-button');
if (!li) return;
const tag = nameFromLi(li);
if (!tag) return;
const observer = new MutationObserver((mutations, obs) => {
const nativeMenu = document.querySelector('[role="menu"].context-menu');
if (nativeMenu && !nativeMenu.querySelector('[data-mm-injected]')) {
injectIntoNativeMenu(tag, nativeMenu);
obs.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), 400);
}, true);
}
function needsRender() {
const ol = getLocOl();
const ul = getMainUl();
const footer = document.querySelector('.map-meta__actions');
if (footer && !document.getElementById('mm-footer-actions-wrapper')) return true;
if (ul) return !ul.querySelector('[data-mm="1"]');
return false;
}
async function deleteFolder(id) {
const f = findF(id);
if (!f) return;
if (!confirm(`Delete folder "${f.name}"?\n\n(The tags inside will be moved back to the main list, they won't be deleted from your map)`)) return;
delF(id);
saveS();
reRenderFolders();
}
function buildFolder(folder, depth = 0, isLast = false, parentGradColor = null) {
const li = mk('li', '', '', { 'data-mm': '1', 'data-mm-folder': folder.id });
folder._inheritedColor = parentGradColor || null;
const bg = (parentGradColor && folder.useParentColor !== false) ? parentGradColor : folder.color || '#c0f0f8';
const fg = textOn(bg);
const gradArr = folder.gradient || [folder.colorStart || '#3b82f6', folder.colorEnd || '#06b6d4'];
const grad = `linear-gradient(90deg, ${gradArr.join(', ')})`;
const colors = gradColors(folder);
const fullyOn = isFolderFullyOn(folder);
const partialOn = !fullyOn && isFolderPartiallyOn(folder);
const hdClass = fullyOn ? ' mm-hd--on' : (partialOn ? ' mm-hd--partial' : ' mm-hd--off');
const hd = mk('div', 'mm-hd' + hdClass);
hd.style.backgroundColor = bg;
hd.style.color = fg;
dndOn(hd, folder.id);
hd.setAttribute('draggable', 'true');
hd.addEventListener('dragstart', e => {
if (dragTag) return;
if (e.target.closest('.mm-left-actions, .mm-right-actions')) { e.preventDefault(); return; }
e.stopPropagation();
dragFolderId = folder.id;
setTimeout(() => { if (dragFolderId) li.style.opacity = '.5'; }, 0);
try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); } catch (_) {}
});
hd.addEventListener('dragend', () => { li.style.opacity = ''; dragFolderId = null; });
li.addEventListener('dragover', e => {
if (!dragFolderId || dragFolderId === folder.id || dragTag) return;
if (isDescendant(dragFolderId, folder.id)) return;
e.preventDefault(); e.stopPropagation();
const rect = hd.getBoundingClientRect();
const isTopHalf = e.clientY < rect.top + rect.height / 2;
if (isTopHalf) {
li.classList.add('mm-drop-above'); li.classList.remove('mm-drop-inside', 'mm-drop-after');
} else if (isLast && e.clientY > rect.bottom - 6) {
li.classList.add('mm-drop-after'); li.classList.remove('mm-drop-above', 'mm-drop-inside');
} else {
li.classList.add('mm-drop-inside'); li.classList.remove('mm-drop-above', 'mm-drop-after');
}
});
li.addEventListener('dragleave', e => {
if (!li.contains(e.relatedTarget)) li.classList.remove('mm-drop-above', 'mm-drop-inside', 'mm-drop-after');
});
li.addEventListener('drop', e => {
const wasAbove = li.classList.contains('mm-drop-above');
const wasAfter = li.classList.contains('mm-drop-after');
li.classList.remove('mm-drop-above', 'mm-drop-inside', 'mm-drop-after');
if (!dragFolderId || dragFolderId === folder.id || dragTag) return;
if (isDescendant(dragFolderId, folder.id)) return;
e.preventDefault(); e.stopPropagation();
const id = dragFolderId; dragFolderId = null;
if (wasAbove) reorderFolder(id, folder.id);
else if (wasAfter) { const dragged = extractFolder(id); if (dragged) { S.folders.push(dragged); saveS(); reRenderFolders(); } }
else moveFolderInto(id, folder.id);
});
const leftActions = mk('div', 'mm-left-actions');
const az = mk('div', 'mm-arrow-zone');
az.innerHTML = SVG_ARR(folder.expanded ? 90 : 0);
az.onclick = e => {
e.stopPropagation();
folder.expanded = !folder.expanded;
saveS(false);
// Rotation de la flèche
const svg = az.querySelector('svg');
if (svg) svg.style.transform = `rotate(${folder.expanded ? 90 : 0}deg)`;
if (!folder.expanded) {
// Collapse : retire juste le contenu de ce dossier
li.querySelectorAll(':scope > .mm-sub-wrap, :scope > .mm-body').forEach(el => el.remove());
} else {
// Expand : construit uniquement le contenu de ce dossier
if (folder.children?.length) {
const sw = mk('div', 'mm-sub-wrap', '', { 'data-mm': '1' });
const childColors = gradChildColors(folder);
folder.children.forEach((c, ci) => sw.appendChild(buildFolder(c, depth + 1, false, childColors[ci] || null)));
li.appendChild(sw);
}
const body = mk('div', 'mm-body', '', { 'data-mm': '1' });
dndOn(body, folder.id);
const tagColors = gradColors(folder);
if (!folder.tags.length) {
const dz = mk('div', 'mm-dropzone');
dz.textContent = '↓ Drop tags here';
dndOn(dz, folder.id);
body.appendChild(dz);
} else {
folder.tags.forEach((tag, i) => body.appendChild(buildTagBtn(tag, folder.gradTags !== false ? (tagColors[i] || null) : null)));
}
const addSub = mk('button', 'mm-subaddbtn', '', { 'data-mm': '1' });
addSub.textContent = '+ New subfolder';
addSub.onclick = ev => { ev.stopPropagation(); newFolder(folder.id); };
body.appendChild(addSub);
li.appendChild(body);
}
};
const pen = mk('span', 'mm-pencil'); pen.innerHTML = SVG_PEN; pen.title = 'Edit'; pen.style.color = fg;
pen.onclick = e => { e.stopPropagation(); editFolder(folder.id); };
leftActions.append(az, pen);
const nameSp = mk('span', '', 'flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;');
nameSp.textContent = folder.name;
const gd = mk('div', 'mm-grad'); gd.style.background = grad;
const cnt = mk('span', 'mm-cnt'); cnt.style.color = fg;
const tagCount = allTagsOf(folder).length;
const locCount = allTagsOf(folder).reduce((sum, tag) => sum + (parseInt(countFromLi(liOfTag(tag))) || 0), 0);
cnt.textContent = locCount > 0 ? `${tagCount} · ${locCount} locs` : tagCount;
const rightActions = mk('div', 'mm-right-actions');
const colBtn = mk('button', 'mm-actbtn');
colBtn.textContent = 'Apply colors';
colBtn.title = 'Apply gradient colors to native tags';
colBtn.style.cssText += ';font-size:10px;padding:1px 8px;color:' + fg;
colBtn.onclick = async e => {
e.stopPropagation();
const n = allTagsOf(folder).length;
if (!n) return;
const secs = Math.ceil(n * 0.5);
const timeStr = secs < 60 ? `~${secs} seconds` : `~${Math.ceil(secs / 60)} minutes`;
if (confirm(`Apply gradient to the ${n} tags of "${folder.name}"?\n\n⚠️ This will modify the actual tag colors in the app and take ${timeStr}.\n\nPlease DO NOT click anything until the button says 'Done!'.`)) {
colBtn.textContent = 'Applying...'; colBtn.style.opacity = '0.5'; colBtn.style.pointerEvents = 'none';
const failed = await applyFolderColorsToNative(folder);
colBtn.textContent = failed > 0 ? `Done (${failed} failed)` : 'Done!';
if (failed > 0) console.warn(`[mm-folders] ${failed} tag(s) could not be recolored.`);
setTimeout(() => { colBtn.textContent = 'Apply colors'; colBtn.style.opacity = '1'; colBtn.style.pointerEvents = 'auto'; }, 2500);
}
};
const delBtn = mk('button', 'mm-actbtn'); delBtn.textContent = '✕'; delBtn.title = 'Delete'; delBtn.style.color = fg;
delBtn.onclick = e => { e.stopPropagation(); deleteFolder(folder.id); };
rightActions.append(colBtn, delBtn);
hd.append(leftActions, nameSp, gd, cnt, rightActions);
hd.onclick = e => {
if (e.target.closest('.mm-arrow-zone,.mm-pencil,.mm-actbtn')) return;
e.stopPropagation(); e.preventDefault();
toggleVis(folder);
};
li.appendChild(hd);
if (folder.expanded) {
if (folder.children?.length) {
const sw = mk('div', 'mm-sub-wrap', '', { 'data-mm': '1' });
const childColors = gradChildColors(folder);
folder.children.forEach((c, ci) => sw.appendChild(buildFolder(c, depth + 1, false, childColors[ci] || null)));
li.appendChild(sw);
}
const body = mk('div', 'mm-body', '', { 'data-mm': '1' });
dndOn(body, folder.id);
if (!folder.tags.length) {
const dz = mk('div', 'mm-dropzone'); dz.textContent = '↓ Drop tags here'; dndOn(dz, folder.id); body.appendChild(dz);
} else {
folder.tags.forEach((tag, i) => body.appendChild(buildTagBtn(tag, folder.gradTags !== false ? (colors[i] || null) : null)));
}
const addSub = mk('button', 'mm-subaddbtn', '', { 'data-mm': '1' });
addSub.textContent = '+ New subfolder';
addSub.onclick = e => { e.stopPropagation(); newFolder(folder.id); };
body.appendChild(addSub);
li.appendChild(body);
}
return li;
}
function renderInto(container) {
if (!container) return;
container.querySelectorAll('[data-mm="1"]').forEach(e => e.remove());
container.querySelectorAll('.mm-sep,.mm-addli').forEach(e => e.remove());
invalidateFlatCache();
const addLi = mk('li', 'mm-addli', '', { 'data-mm': '1' });
const addBtn = mk('button', 'mm-addbtn'); addBtn.innerHTML = '<span>+ New folder</span>';
addBtn.onclick = () => newFolder(null);
addBtn.addEventListener('dragover', e => { if (dragTag) { e.preventDefault(); addBtn.style.outline = '2px dashed rgba(0,0,0,.3)'; } });
addBtn.addEventListener('dragleave', () => addBtn.style.outline = '');
addBtn.addEventListener('drop', async e => {
e.preventDefault(); addBtn.style.outline = '';
const tag = dragTag || e.dataTransfer.getData('text/plain'); if (!tag) return;
const r = await modal({
title: `New folder for "${tag}"`,
fields: [{ key: 'name', label: 'Name', type: 'text', placeholder: 'Folder name' }],
showFolderColor: true, folderColor: '#c0f0f8',
showColors: true, colors: ['#c0f0f8', '#183848'],
okText: 'Create'
});
if (!r?.name?.trim()) return;
const gradientColors = r.gradientColors || ['#c0f0f8', '#183848'];
const f = {
id: uid(), name: r.name.trim(),
color: r.folderColor || '#c0f0f8',
gradient: gradientColors, colorStart: gradientColors[0], colorEnd: gradientColors[gradientColors.length - 1],
noGrad: !!r.noGrad, gradTags: r.gradTags !== false, gradFolders: !!r.gradFolders,
expanded: true, visible: true, tags: [tag], children: []
};
S.folders.push(f); dragTag = null; saveS(); reRenderFolders();
});
const exportBtn = mk('button', 'mm-actbtn', 'margin-left:6px;color:#a0a0a0;font-size:10px;background:transparent;');
exportBtn.textContent = 'Export'; exportBtn.title = 'Export folders (local JSON)';
exportBtn.onclick = () => {
const blob = new Blob([JSON.stringify(S, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `mm-folders-${getMapId() || 'map'}.json`;
a.click(); URL.revokeObjectURL(url);
};
const importBtn = mk('button', 'mm-actbtn', 'margin-left:4px;color:#a0a0a0;font-size:10px;background:transparent;');
importBtn.textContent = 'Import'; importBtn.title = 'Import folders (local JSON)';
importBtn.onclick = () => {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
const parsed = JSON.parse(ev.target.result);
if (!parsed.folders) { alert('Invalid file — no folders found.'); return; }
if (!confirm(`Import ${parsed.folders.length} folder(s)?\n(Current folders will be replaced.)`)) return;
S = { folders: parsed.folders };
saveS(); reRenderFolders();
} catch (_) { alert('JSON file read error.'); }
};
reader.readAsText(file);
};
input.click();
};
const infoBtn = mk('button', 'mm-actbtn', 'margin-left:4px;background:transparent;padding:2px;cursor:pointer;display:flex;align-items:center;');
infoBtn.title = 'Help & Warnings';
infoBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" style="fill:#a0a0a0;"><path d="M12 2C6.489 2 2 6.489 2 12s4.489 10 10 10 10-4.489 10-10S17.511 2 12 2zm0 2c4.43 0 8 3.57 8 8s-3.57 8-8 8-8-3.57-8-8 3.57-8 8-8zm-1 5v2h2V9h-2zm0 4v6h2v-6h-2z"/></svg>`;
infoBtn.onclick = showInfoModal;
const row = mk('div', '', 'display:flex;align-items:center;margin-bottom:2px;');
row.append(addBtn, exportBtn, importBtn, infoBtn);
addLi.appendChild(row);
container.insertBefore(addLi, container.firstChild);
let last = addLi;
S.folders.forEach((f, i) => {
const li = buildFolder(f, 0, i === S.folders.length - 1);
last.insertAdjacentElement('afterend', li);
last = li;
});
if (S.folders.length) {
const sep = mk('li', 'mm-sep', '', { 'data-mm': '1' });
last.insertAdjacentElement('afterend', sep);
const dropOutLi = mk('li', '', 'list-style:none;', { 'data-mm': '1' });
const dropOut = mk('div', '', 'padding:4px 8px;border:1.5px dashed rgba(255,255,255,.15);border-radius:8px;font-size:11px;font-family:"Open Sans",sans-serif;color:rgba(255,255,255,.25);text-align:center;font-style:italic;transition:border-color .15s,color .15s;margin-bottom:4px;');
dropOut.textContent = '↓ Drop here to remove from folder';
dropOut.addEventListener('dragover', e => { if (!dragTag) return; e.preventDefault(); dropOut.style.borderColor = 'rgba(255,100,100,.6)'; dropOut.style.color = 'rgba(255,150,150,.8)'; });
dropOut.addEventListener('dragleave', () => { dropOut.style.borderColor = ''; dropOut.style.color = ''; });
dropOut.addEventListener('drop', e => {
e.preventDefault(); dropOut.style.borderColor = ''; dropOut.style.color = '';
const tag = dragTag || e.dataTransfer.getData('text/plain');
if (tag) { dragTag = null; removeTag(tag); }
});
dropOutLi.appendChild(dropOut);
sep.insertAdjacentElement('afterend', dropOutLi);
}
}
function render() {
if (!getMapId()) return;
injectFooterButtons();
const ol = getLocOl();
const ul = getMainUl();
if (ol) {
document.body.classList.add('mm-loc-open');
if (ul && !ul.querySelector('[data-mm="1"]')) renderInto(ul);
} else {
document.body.classList.remove('mm-loc-open');
if (ul) renderInto(ul);
}
updateHideStyle();
}
function watchDOM() {
let _t = null;
let colorSyncInterval = null;
new MutationObserver(mutations => {
let needsColorSync = false;
let syncRender = false;
for (const m of mutations) {
if (m.removedNodes.length) {
m.removedNodes.forEach(n => {
if (n.nodeType !== 1) return;
if (n.getAttribute?.('role') === 'dialog' || n.classList?.contains('edit-tag-modal')) {
needsColorSync = true;
}
// Détection suppression native :
if (n.tagName === 'LI' && n.classList?.contains('tag') && n.classList?.contains('has-button') && !n.hasAttribute('data-mm')) {
const tagName = nameFromLi(n);
// Si le tag était dans nos dossiers
if (tagName && !tagName.startsWith(STORAGE_PREFIX) && !tagName.startsWith(OLD_STORAGE_PREFIX) && getAssignedTags().has(tagName)) {
// On attend plus longtemps pour s'assurer que c'est une vraie suppression et non un re-render dû à la recherche
setTimeout(() => {
const currentlySearching = document.body.classList.contains('mm-search-mode') || document.body.classList.contains('mm-search-exiting');
const mainUl = getMainUl();
// Vérifie qu'on n'est pas en train de chercher, que la liste est bien chargée, et que le tag a vraiment disparu du DOM natif
if (!currentlySearching && mainUl && !liOfTag(tagName)) {
removeTag(tagName);
}
}, 1000); // Délai de 1s pour laisser le DOM natif se stabiliser
}
}
});
}
if (m.addedNodes.length) {
m.addedNodes.forEach(n => {
if (n.nodeType !== 1) return;
if (n.tagName === 'UL' && n.classList.contains('tag-list') && !n.hasAttribute('role')) syncRender = true;
const processNode = el => {
if (el.tagName === 'LI' && !el.hasAttribute('data-mm')) {
const ul = el.closest('ul.tag-list:not([role="listbox"])');
if (ul && !ul.querySelector('[data-mm="1"]')) syncRender = true;
const txt = el.textContent || '';
if (txt.includes(STORAGE_PREFIX) || txt.includes(OLD_STORAGE_PREFIX)) {
el.style.setProperty('display', 'none', 'important');
} else if (!document.body.classList.contains('mm-search-mode')) {
const name = nameFromLi(el);
if (name && getAssignedTags().has(name)) el.style.setProperty('display', 'none', 'important');
}
}
};
processNode(n);
if (n.querySelectorAll) n.querySelectorAll('li:not([data-mm])').forEach(processNode);
});
}
}
if (syncRender && needsRender()) render();
if (needsColorSync) {
clearInterval(colorSyncInterval);
let ticks = 0;
colorSyncInterval = setInterval(() => {
document.querySelectorAll('li:not([data-mm])').forEach(li => delete li.dataset.mmColor);
updateHideStyle();
ticks++;
if (ticks > 6) clearInterval(colorSyncInterval);
}, 250);
}
clearTimeout(_t);
_t = setTimeout(() => {
if (needsRender()) render();
else updateHideStyle();
}, 50);
}).observe(document.body, { childList: true, subtree: true });
setInterval(() => { if (needsRender()) render(); }, 5000);
}
function watchNav() {
let last = location.href;
const check = () => {
if (location.href === last) return;
last = location.href;
document.getElementById('mm-hide-css')?.remove();
invalidateFlatCache();
loadS();
setTimeout(() => render(), 700);
};
window.addEventListener('popstate', check);
const orig = history.pushState.bind(history);
history.pushState = (...a) => { orig(...a); check(); };
setInterval(check, 1500);
}
function boot() {
document.body.classList.add('mm-loading');
loadS();
injectCSS();
initDnD();
bindCtxMenu();
bindTooltipListeners();
watchSearchInput();
watchNav();
watchDOM();
window.addEventListener('beforeunload', e => {
if (!isDirty) return;
e.preventDefault();
e.returnValue = '';
});
setTimeout(() => document.body.classList.remove('mm-loading'), 3000);
let tries = 0;
function tryRender() {
tries++;
if (document.querySelectorAll('li.tag.has-button').length > 0 || document.querySelector('.map-meta__actions')) {
loadFromCloud(false);
render();
} else if (tries < 25) {
setTimeout(tryRender, 500);
}
}
const obs = new MutationObserver(() => {
if (document.querySelectorAll('li.tag.has-button').length > 0 || document.querySelector('.map-meta__actions')) {
obs.disconnect();
setTimeout(tryRender, 80);
}
});
obs.observe(document.body, { childList: true, subtree: true });
setTimeout(tryRender, 800);
setTimeout(tryRender, 2200);
}
boot();
})();