1/** 2 * PaletteEngine - Dynamic Color Palette Swapping, Cross-Subdomain Cookie Engine, 3 * and Real-Time Tab Synchronization for SOL.VIN (Deterministic Indexing) 4 */ 5(function(window) { 6 'use strict'; 7 8 var PaletteEngine = { 9 palettes: [], 10 currentPalette: null, 11 currentIndex: 0, 12 STORAGE_KEY: 'solvin_palette_id', 13 CHANNEL_NAME: 'solvin_palette_channel', 14 channel: null, 15 16 /** 17 * Resolves multi-label root domain for setting shared domain cookies across subdomains. 18 * Only returns domains with dots (e.g. sol.vin, dev.sol.vin, lvh.me) because modern 19 * browsers reject domain= attributes on single-label domains like localhost. 20 */ 21 getRootDomain: function() { 22 var host = window.location.hostname; 23 if (!host) return null; 24 var isIp = /^(\d{1,3}\.){3}\d{1,3}$/.test(host) || host.includes(':'); 25 if (isIp || host === 'localhost' || host.endsWith('.localhost')) return null; 26 var parts = host.split('.'); 27 if (parts.length < 2) return null; 28 return parts.slice(-2).join('.'); 29 }, 30 31 /** 32 * Reads a cookie value by name from document.cookie. 33 */ 34 getCookie: function(name) { 35 var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')); 36 return match ? decodeURIComponent(match[1]) : null; 37 }, 38 39 /** 40 * Writes a cookie. Scopes to root domain if multi-label domain (e.g. domain=sol.vin). 41 */ 42 setCookie: function(name, value, days) { 43 var rootDomain = this.getRootDomain(); 44 var expires = days ? '; max-age=' + (days * 86400) : ''; 45 var secure = window.location.protocol === 'https:' ? '; Secure' : ''; 46 var cookieStr = name + '=' + encodeURIComponent(value) + expires + '; path=/; SameSite=Lax' + secure; 47 if (rootDomain) { 48 cookieStr += '; domain=' + rootDomain; 49 } 50 document.cookie = cookieStr; 51 }, 52 53 /** 54 * Retrieves saved palette ID strictly from cookies. 55 */ 56 getSavedPaletteId: function() { 57 return this.getCookie(this.STORAGE_KEY); 58 }, 59 60 /** 61 * Resolves variable color keys against defined color dictionary. 62 */ 63 getResolvedColors: function(palette) { 64 if (!palette) return {}; 65 if (!palette.variables && palette.colors) { 66 return palette.colors; 67 } 68 var colorDefs = palette.palette_colors || palette.colors || {}; 69 var vars = palette.variables || palette.colors || {}; 70 var resolved = {}; 71 for (var key in vars) { 72 if (Object.prototype.hasOwnProperty.call(vars, key)) { 73 var val = vars[key]; 74 if (typeof val === 'object' && val !== null) { 75 val = val.value; 76 } 77 if (typeof val === 'string' && colorDefs[val]) { 78 resolved[key] = (typeof colorDefs[val] === 'object' && colorDefs[val] !== null) ? colorDefs[val].value : colorDefs[val]; 79 } else { 80 resolved[key] = val; 81 } 82 } 83 } 84 return resolved; 85 }, 86 87 /** 88 * Applies CSS variable overrides to documentElement and updates active dataset attributes. 89 */ 90 applyPalette: function(palette) { 91 if (!palette) return; 92 var colors = this.getResolvedColors(palette); 93 if (!colors) return; 94 var root = document.documentElement; 95 for (var key in colors) { 96 if (Object.prototype.hasOwnProperty.call(colors, key)) { 97 var val = colors[key]; 98 var cssVarName = '--' + key.replace(/_/g, '-'); 99 root.style.setProperty(cssVarName, val); 100 } 101 } 102 if (!colors.cube) { 103 var textVal = colors.cube_stroke_color || colors.text_color || '#1c1c1e'; 104 root.style.setProperty('--cube', textVal); 105 } 106 if (!colors.cube_hover) { 107 var hoverVal = colors.border_active || colors.accent_color || colors.text_color || '#1c1c1e'; 108 root.style.setProperty('--cube-hover', hoverVal); 109 } 110 root.style.setProperty('--cube-stroke-color', colors.cube || colors.cube_stroke_color || colors.text_color || '#1c1c1e'); 111 root.setAttribute('data-palette', palette.id || ''); 112 this.updateButtonLabel(palette); 113 this.updateDuotoneFilter(palette); 114 this.refreshChartColors();
115 try { 116 if (typeof document !== 'undefined' && document.dispatchEvent) { 117 document.dispatchEvent(new CustomEvent('palettechange', { detail: { palette: palette } })); 118 } 119 } catch(e) {} 120 }, 121 122 /** 123 * Refreshes all active Chart.js chart instances on the page if Chart.js is loaded. 124 */ 125 refreshChartColors: function() { 126 if (typeof window !== 'undefined' && window.Chart && window.Chart.instances) { 127 var instances = window.Chart.instances; 128 for (var id in instances) { 129 if (Object.prototype.hasOwnProperty.call(instances, id)) { 130 var chart = instances[id]; 131 if (chart && typeof chart.update === 'function') { 132 chart.update(); 133 } 134 } 135 } 136 } 137 }, 138 139 /** 140 * Parses CSS hex/rgb color string into normalized RGB values [0..1]. 141 */ 142 parseColorToRgb: function(colorStr) { 143 if (colorStr === null || colorStr === undefined) return { r: 0, g: 0, b: 0 }; 144 if (typeof colorStr !== 'string') { 145 if (typeof colorStr === 'object' && colorStr !== null && colorStr.value) { 146 colorStr = String(colorStr.value); 147 } else { 148 colorStr = String(colorStr); 149 } 150 } 151 colorStr = colorStr.trim(); 152 if (!colorStr) return { r: 0, g: 0, b: 0 }; 153 154 var cleanHex = colorStr.startsWith('#') ? colorStr.substring(1) : colorStr; 155 if (/^[0-9a-fA-F]{3}$/.test(cleanHex) || /^[0-9a-fA-F]{6}$/.test(cleanHex)) { 156 if (cleanHex.length === 3) { 157 cleanHex = cleanHex[0] + cleanHex[0] + cleanHex[1] + cleanHex[1] + cleanHex[2] + cleanHex[2]; 158 } 159 var num = parseInt(cleanHex, 16); 160 if (!isNaN(num)) { 161 return { 162 r: ((num >> 16) & 255) / 255, 163 g: ((num >> 8) & 255) / 255, 164 b: (num & 255) / 255 165 }; 166 } 167 } 168 var match = colorStr.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); 169 if (match) { 170 return { 171 r: parseInt(match[1], 10) / 255, 172 g: parseInt(match[2], 10) / 255, 173 b: parseInt(match[3], 10) / 255 174 }; 175 } 176 return { r: 0, g: 0, b: 0 }; 177 }, 178 179 /** 180 * Calculates feColorMatrix 5x4 matrix values string for interpolating between 181 * duotone color palette mapping (progress t=0.0) and full-color original image (progress t=1.0). 182 */ 183 getDuotoneMatrixValues: function(palette, t) { 184 t = Math.max(0, Math.min(1, t)); 185 var startVal = '#1c1c1e'; 186 var endVal = '#faf6ee'; 187 188 var colors = this.getResolvedColors(palette); 189 if (colors && Object.keys(colors).length > 0) { 190 if (colors.filter_start_color) { 191 startVal = colors.filter_start_color; 192 } else if (colors.text_color) { 193 startVal = colors.text_color; 194 } 195 if (colors.filter_end_color) { 196 endVal = colors.filter_end_color; 197 } else if (colors.bg_color) { 198 endVal = colors.bg_color; 199 } 200 } else if (typeof window !== 'undefined' && window.getComputedStyle) { 201 var rootStyles = getComputedStyle(document.documentElement); 202 if (rootStyles) { 203 var getVar = function(prop) { 204 var raw = rootStyles.getPropertyValue(prop); 205 return (typeof raw === 'string' && raw) ? raw.trim() : ''; 206 }; 207 startVal = getVar('--filter-start-color') || getVar('--text-color') || startVal; 208 endVal = getVar('--filter-end-color') || getVar('--bg-color') || endVal; 209 } 210 } 211 212 var bg = this.parseColorToRgb(endVal); 213 var txt = this.parseColorToRgb(startVal); 214 215 var dr = bg.r - txt.r; 216 var dg = bg.g - txt.g; 217 var db = bg.b - txt.b; 218 219 var invT = 1 - t; 220 221 var rR = invT * dr * 0.2126 + t; 222 var rG = invT * dr * 0.7152; 223 var rB = invT * dr * 0.0722; 224 var rC = invT * txt.r; 225 226 var gR = invT * dg * 0.2126; 227 var gG = invT * dg * 0.7152 + t; 228 var gB = invT * dg * 0.0722; 229 var gC = invT * txt.g; 230 231 var bR = invT * db * 0.2126; 232 var bG = invT * db * 0.7152; 233 var bB = invT * db * 0.0722 + t; 234 var bC = invT * txt.b; 235 236 return [ 237 rR.toFixed(4), rG.toFixed(4), rB.toFixed(4), 0, rC.toFixed(4), 238 gR.toFixed(4), gG.toFixed(4), gB.toFixed(4), 0, gC.toFixed(4), 239 bR.toFixed(4), bG.toFixed(4), bB.toFixed(4), 0, bC.toFixed(4), 240 0, 0, 0, 1, 0 241 ].join(' '); 242 }, 243 244 /** 245 * Updates SVG feColorMatrix values with current palette text and bg colors. 246 */ 247 updateDuotoneFilter: function(palette) { 248 var self = this; 249 this._activePalette = palette; 250 var globalMatrix = document.getElementById('palette-duotone-matrix'); 251 if (globalMatrix) {
252 globalMatrix.setAttribute('values', this.getDuotoneMatrixValues(palette, 0)); 253 } 254 255 var svgContainer = document.getElementById('duotone-svg-container'); 256 if (!svgContainer) return; 257 258 var activeFilters = svgContainer.querySelectorAll('feColorMatrix[data-duotone-matrix]'); 259 for (var f = 0; f < activeFilters.length; f++) { 260 var mat = activeFilters[f]; 261 var img = mat._boundImg; 262 var currentT = (img && typeof img._duotoneProgress === 'number') ? img._duotoneProgress : 0; 263 mat.setAttribute('values', self.getDuotoneMatrixValues(palette, currentT)); 264 } 265 }, 266 267 /** 268 * Updates the UI button text label if element is present in DOM. 269 */ 270 updateButtonLabel: function(palette) { 271 var label = document.getElementById('palette-name-label'); 272 if (label && palette) { 273 label.textContent = palette.name || palette.id; 274 } 275 var preview = document.getElementById('palette-color-preview'); 276 if (preview && palette) { 277 var colors = this.getResolvedColors(palette); 278 var secColor = colors.accent_secondary || colors.accent_color || colors.text_color; 279 if (secColor) preview.style.color = secColor; 280 } 281 }, 282 283 openPaletteModal: function() { 284 var modalOverlay = document.getElementById('palette-picker-modal-overlay'); 285 var modalGrid = document.getElementById('palette-picker-grid'); 286 if (!modalOverlay || !modalGrid) return; 287 288 this.renderPaletteGrid(); 289 modalOverlay.style.cssText = 'display: flex !important; z-index: 999999;'; 290 document.body.style.overflow = 'hidden'; 291 }, 292 293 closePaletteModal: function() { 294 var modalOverlay = document.getElementById('palette-picker-modal-overlay'); 295 if (!modalOverlay) return; 296 modalOverlay.style.cssText = 'display: none !important;'; 297 document.body.style.overflow = ''; 298 }, 299 300 renderPaletteGrid: function() { 301 var modalGrid = document.getElementById('palette-picker-grid'); 302 if (!modalGrid || !this.palettes) return; 303 304 var self = this; 305 modalGrid.innerHTML = ''; 306 307 for (var i = 0; i < this.palettes.length; i++) { 308 (function(idx) { 309 var pal = self.palettes[idx]; 310 var btn = document.createElement('button'); 311 btn.type = 'button'; 312 btn.className = 'export-btn palette-grid-item-btn'; 313 314 var colors = self.getResolvedColors(pal); 315 var bg = colors.bg_color || '#faf6ee'; 316 var txt = colors.text_color || '#1c1c1e'; 317 var border = colors.border_color || '#1c1c1e'; 318 var shadow = colors.shadow_color || border; 319 var circleColor = colors.accent_secondary || colors.accent_color || txt; 320 321 btn.style.cssText = 'background: ' + bg + ' !important; color: ' + txt + ' !important; border: 2px solid ' + border + ' !important; box-shadow: 2px 2px 0px ' + shadow + ' !important; padding: 0.6rem 0.8rem; font-size: 0.85rem; font-weight: bold; cursor: pointer; text-align: left; display: flex; align-items: center; justify-content: space-between; font-family: monospace; border-radius: 3px;'; 322 323 if (idx === self.currentIndex) { 324 btn.style.outline = '2px solid ' + txt; 325 } 326 327 btn.innerHTML = '<span style="display: flex; align-items: center; gap: 0.4rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">' + 328 '<span style="color: ' + circleColor + '; font-size: 0.9rem;">â</span> ' + 329 '<span>' + (pal.name || pal.id) + '</span>' + 330 '</span>' + 331 '<span style="font-size: 0.75rem; opacity: 0.75;">[ #' + (idx + 1) + ' ]</span>'; 332 333 btn.addEventListener('click', function(e) { 334 e.preventDefault(); 335 self.selectIndex(idx); 336 self.closePaletteModal(); 337 }); 338 339 modalGrid.appendChild(btn); 340 })(i); 341 } 342 }, 343 344 findPaletteIndexById: function(id) { 345 if (!id || !this.palettes) return -1; 346 for (var i = 0; i < this.palettes.length; i++) { 347 if (this.palettes[i].id === id) return i; 348 } 349 return -1; 350 }, 351 352 selectIndex: function(idx) { 353 if (!this.palettes || this.palettes.length === 0) return; 354 var safeIdx = (idx % this.palettes.length + this.palettes.length) % this.palettes.length; 355 this.currentIndex = safeIdx; 356 this.currentPalette = this.palettes[safeIdx]; 357 this.applyPalette(this.currentPalette); 358
359 if (this.currentPalette && this.currentPalette.id) { 360 this.setCookie(this.STORAGE_KEY, this.currentPalette.id, 365); 361 if (this.channel) { 362 try { this.channel.postMessage({ id: this.currentPalette.id, index: safeIdx }); } catch(e) {} 363 } 364 } 365 }, 366 367 nextPalette: function() { 368 if (!this.palettes || this.palettes.length === 0) return; 369 this.selectIndex(this.currentIndex + 1); 370 }, 371 372 prevPalette: function() { 373 if (!this.palettes || this.palettes.length === 0) return; 374 this.selectIndex(this.currentIndex - 1); 375 }, 376 377 resetDefaultPalette: function() { 378 this.selectIndex(0); 379 }, 380 381 randomPalette: function() { 382 if (!this.palettes || this.palettes.length === 0) return; 383 if (this.palettes.length === 1) { 384 this.selectIndex(0); 385 return; 386 } 387 var randomIdx = this.currentIndex; 388 do { 389 randomIdx = Math.floor(Math.random() * this.palettes.length); 390 } while (randomIdx === this.currentIndex); 391 this.selectIndex(randomIdx); 392 }, 393 394 syncFromStorage: function() { 395 var savedId = this.getSavedPaletteId(); 396 if (savedId) { 397 var idx = this.findPaletteIndexById(savedId); 398 if (idx >= 0 && idx !== this.currentIndex) { 399 this.selectIndex(idx); 400 } 401 } 402 }, 403 404 initHead: function(palettes) { 405 this.palettes = palettes || window.SITE_PALETTES || []; 406 var savedId = this.getSavedPaletteId(); 407 var idx = this.findPaletteIndexById(savedId); 408 if (idx < 0) idx = 0; 409 this.selectIndex(idx); 410 }, 411 412 initDuotoneImages: function(scope) { 413 scope = scope || document; 414 var self = this; 415 416 // 1. Unwrap any legacy wrapper spans (.photo-filter-container) and remove legacy overlay elements 417 var containers = scope.querySelectorAll('.photo-filter-container'); 418 for (var c = 0; c < containers.length; c++) { 419 var cont = containers[c]; 420 var realImg = cont.querySelector('img:not(.photo-filter-overlay)'); 421 if (realImg && cont.parentNode) { 422 cont.parentNode.insertBefore(realImg, cont); 423 } 424 cont.remove(); 425 } 426 var legacyOverlays = scope.querySelectorAll('.photo-filter-overlay, .duotone-overlay-img'); 427 for (var k = 0; k < legacyOverlays.length; k++) { 428 legacyOverlays[k].remove(); 429 } 430 431 var svgContainer = document.getElementById('duotone-svg-container'); 432 if (!svgContainer) { 433 svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 434 svgContainer.id = 'duotone-svg-container'; 435 svgContainer.setAttribute('aria-hidden', 'true'); 436 svgContainer.style.cssText = 'position: absolute; width: 0; height: 0; overflow: hidden; opacity: 0; pointer-events: none;'; 437 document.body.appendChild(svgContainer); 438 } 439 440 if (!self._duotoneCounter) self._duotoneCounter = 0; 441 442 // 2. Query all img tags 443 var imgs = scope.querySelectorAll('img'); 444 for (var i = 0; i < imgs.length; i++) { 445 var img = imgs[i]; 446 447 // 3. Check for explicit filter opt-out attribute filter="0" or data-filter="0" 448 if (img.getAttribute('filter') === '0' || img.getAttribute('data-filter') === '0') { 449 img.classList.remove('duotone-filtered'); 450 img.style.filter = 'none'; 451 img.style.webkitFilter = 'none'; 452 continue; 453 } 454 455 // 4. Check for opt-out container classes / attributes or Image Viewer Modal 456 if (img.id === 'image-viewer-img' || 457 img.closest('#gui-image-modal, .image-viewer-window, .image-viewer-body, [data-monochrome="false"], [data-palette-filter="false"], .no-monochrome, .no-palette-filter, .is-pdf, .pdf-resume-container, .is-export')) { 458 img.classList.remove('duotone-filtered'); 459 img.style.filter = 'none'; 460 img.style.webkitFilter = 'none'; 461 continue; 462 } 463 464 // 5. Skip icons, SVGs, header logo 465 var src = (img.currentSrc || img.src || '').toLowerCase(); 466 if (img.closest('.terminal-logo, .terminal-header, #header-cube-app, .render-svg-icon, .skill-icon-svg, .article-link-brand-icon, .rss-link-btn, .accolade-link-btn') || 467 img.classList.contains('article-link-brand-icon') || 468 (src.indexOf('.svg') !== -1 && !img.closest('[data-palette-filter="true"]')) || 469 (src.indexOf('/icons/') !== -1 && !img.closest('[data-palette-filter="true"]'))) { 470 continue; 471 } 472 473 // 6. Assign unique SVG filter for each img 474 if (!img._duotoneFilterId) { 475 self._duotoneCounter++; 476 img._duotoneFilterId = 'duotone-filter-img-' + self._duotoneCounter; 477 478 var filterElem = document.createElementNS('http://www.w3.org/2000/svg', 'filter'); 479 filterElem.setAttribute('id', img._duotoneFilterId); 480 filterElem.setAttribute('color-interpolation-filters', 'sRGB'); 481 482 var matrixElem = document.createElementNS('http://www.w3.org/2000/svg', 'feColorMatrix'); 483 matrixElem.setAttribute('type', 'matrix'); 484 matrixElem.setAttribute('data-duotone-matrix', 'true'); 485 matrixElem._boundImg = img;
486 matrixElem.setAttribute('values', self.getDuotoneMatrixValues(self._activePalette || self.currentPalette, 0)); 487 488 filterElem.appendChild(matrixElem); 489 svgContainer.appendChild(filterElem); 490 491 img._duotoneMatrixElem = matrixElem; 492 img._duotoneProgress = 0; // 0 = duotone, 1 = full color 493 } 494 495 // Apply SVG filter directly to img element 496 img.classList.add('duotone-filtered'); 497 img.style.filter = 'url(#' + img._duotoneFilterId + ')'; 498 img.style.webkitFilter = 'url(#' + img._duotoneFilterId + ')'; 499 500 // 7. Bind 2-second hover transition loop (JS requestAnimationFrame) 501 if (!img._duotoneEventsBound) { 502 img._duotoneEventsBound = true; 503 504 var bindAnimation = function(targetImg) { 505 var startAnim = function(targetProgress) { 506 if (targetImg._duotoneAnimId) { 507 cancelAnimationFrame(targetImg._duotoneAnimId); 508 targetImg._duotoneAnimId = null; 509 } 510 511 var startProgress = typeof targetImg._duotoneProgress === 'number' ? targetImg._duotoneProgress : 0; 512 var duration = 2000; // 2 seconds 513 var startTime = null; 514 515 function step(timestamp) { 516 if (!startTime) startTime = timestamp; 517 var elapsed = timestamp - startTime; 518 var p = Math.min(1, elapsed / duration); 519 520 // Smooth cubic ease-in-out curve 521 var easedP = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; 522 523 targetImg._duotoneProgress = startProgress + (targetProgress - startProgress) * easedP; 524 525 if (targetImg._duotoneMatrixElem) { 526 targetImg._duotoneMatrixElem.setAttribute('values', self.getDuotoneMatrixValues(self._activePalette || self.currentPalette, targetImg._duotoneProgress)); 527 } 528 529 if (p < 1) { 530 targetImg._duotoneAnimId = requestAnimationFrame(step); 531 } else { 532 targetImg._duotoneProgress = targetProgress; 533 targetImg._duotoneAnimId = null; 534 } 535 } 536 537 targetImg._duotoneAnimId = requestAnimationFrame(step); 538 }; 539 540 // Determine hover trigger target: parent gallery wrapper if inside a photo gallery, or the img itself 541 var hoverTarget = targetImg.closest('.photo-carousel-container, .photo-carousel-wrapper, .game-carousel-wrapper, .gallery-main-frame, .carousel-main-frame') || targetImg; 542 543 hoverTarget.addEventListener('mouseenter', function() { 544 startAnim(1.0); // Smooth 2s fade to full color 545 }); 546 547 hoverTarget.addEventListener('mouseleave', function() { 548 startAnim(0.0); // Smooth 2s fade back to duotone 549 }); 550 }; 551 552 bindAnimation(img); 553 } 554 } 555 }, 556 557 initDom: function() { 558 if (this._domInitialized) return; 559 this._domInitialized = true; 560 var self = this; 561 562 if (!this.currentPalette && (!this.palettes || this.palettes.length === 0)) { 563 if (typeof window !== 'undefined' && window.SITE_PALETTES && window.SITE_PALETTES.length > 0) { 564 this.initHead(window.SITE_PALETTES); 565 } 566 } 567 568 if (this.currentPalette) { 569 this.updateButtonLabel(this.currentPalette); 570 this.updateDuotoneFilter(this.currentPalette); 571 } else { 572 this.updateDuotoneFilter(null); 573 } 574 575 this.initDuotoneImages(); 576 577 if ('MutationObserver' in window) { 578 var domObserver = new MutationObserver(function() { 579 self.initDuotoneImages(); 580 }); 581 domObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); 582 } 583 584 try { 585 if ('BroadcastChannel' in window && !this.channel) { 586 this.channel = new BroadcastChannel(this.CHANNEL_NAME); 587 this.channel.onmessage = function(e) { 588 if (e && e.data) { 589 if (typeof e.data.index === 'number' && e.data.index >= 0 && e.data.index < self.palettes.length) { 590 if (e.data.index !== self.currentIndex) { 591 self.selectIndex(e.data.index); 592 } 593 } else if (e.data.id) { 594 var idx = self.findPaletteIndexById(e.data.id); 595 if (idx >= 0 && idx !== self.currentIndex) { 596 self.selectIndex(idx); 597 } 598 } 599 } 600 }; 601 } 602 } catch(e) {} 603 604 window.addEventListener('visibilitychange', function() { 605 if (!document.hidden) self.syncFromStorage(); 606 }); 607 window.addEventListener('focus', function() { 608 self.syncFromStorage(); 609 }); 610 611 var closeBtn = document.getElementById('palette-picker-modal-close'); 612 var modalOverlay = document.getElementById('palette-picker-modal-overlay'); 613 var randomBtn = document.getElementById('palette-picker-random-btn'); 614 615 if (randomBtn) { 616 randomBtn.addEventListener('click', function(e) { 617 e.preventDefault(); 618 self.randomPalette(); 619 }); 620 } 621 if (closeBtn) { 622 closeBtn.addEventListener('click', function(e) { 623 e.preventDefault(); 624 self.closePaletteModal(); 625 }); 626 } 627 if (modalOverlay) { 628 modalOverlay.addEventListener('click', function(e) { 629 if (e.target === modalOverlay) self.closePaletteModal(); 630 }); 631 } 632 document.addEventListener('keydown', function(e) { 633 if (e.key === 'Escape' && modalOverlay && modalOverlay.style.display !== 'none') { 634 self.closePaletteModal(); 635 } 636 }); 637 638 var btn = document.getElementById('palette-swap-btn'); 639 if (btn && window.HoldClick) { 640 HoldClick.attach(btn, { 641 holdDuration: 2000,
642 spinnerDelay: 500, 643 onLeftClick: function() { self.nextPalette(); }, 644 onRightClick: function() { self.prevPalette(); }, 645 onLeftHold: function() { self.openPaletteModal(); }, 646 onRightHold: function() { self.resetDefaultPalette(); } 647 }); 648 } 649 } 650 }; 651 652 window.PaletteEngine = PaletteEngine; 653})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.