1// sys-admin-appearance.js - Admin Click-to-Style Visual Editor 2// Only loads for logged-in admins (detected via _cms_admin hint cookie) 3(function() { 4 'use strict'; 5 var _bp = (typeof __CMS_BASE_PATH !== 'undefined' ? __CMS_BASE_PATH : ''); 6 7 // Gate: only run for admins 8 if (document.cookie.indexOf('_cms_admin=1') === -1) return; 9 10 var PREFIX = 'sys-ap'; 11 12 var DEFAULTS = { 13 text: '#1f2937', heading: '#111827', link: '#2563eb', 14 background: '#ffffff', navBg: '#1a1a1a', navText: '#ffffff', 15 dropdownBg: '#ffffff', dropdownText: '#1f2937', 16 footerBg: '#1a1a1a', footerText: '#ffffff', accent: '#2563eb', border: '#e5e7eb' 17 }; 18 19 var FM = { 20 system: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif", 21 arial: 'Arial,Helvetica,sans-serif', 22 georgia: 'Georgia,serif', 23 times: "'Times New Roman',Times,serif", 24 verdana: 'Verdana,Geneva,sans-serif', 25 tahoma: 'Tahoma,Geneva,sans-serif', 26 trebuchet: "'Trebuchet MS',Helvetica,sans-serif", 27 roboto: 'Roboto,sans-serif', 28 opensans: "'Open Sans',sans-serif", 29 merriweather: 'Merriweather,serif', 30 lato: 'Lato,sans-serif', 31 montserrat: 'Montserrat,sans-serif', 32 poppins: 'Poppins,sans-serif', 33 raleway: 'Raleway,sans-serif', 34 nunito: 'Nunito,sans-serif', 35 playfair: "'Playfair Display',serif" 36 }; 37 38 var GF = { 39 roboto: 'Roboto:wght@400;700', opensans: 'Open+Sans:wght@400;700', 40 merriweather: 'Merriweather:wght@400;700', lato: 'Lato:wght@400;700', 41 montserrat: 'Montserrat:wght@400;700', poppins: 'Poppins:wght@400;700', 42 raleway: 'Raleway:wght@400;700', nunito: 'Nunito:wght@400;700', 43 playfair: 'Playfair+Display:wght@400;700' 44 }; 45 46 var THEME_FILTERS = { 47 light: '', dark: 'invert(1) hue-rotate(180deg)', 48 sepia: 'sepia(0.5) saturate(1.2) brightness(0.92)', 49 highContrast: 'invert(1) hue-rotate(180deg) contrast(1.3)' 50 }; 51 var INVERT_THEMES = { dark: true, highContrast: true }; 52 53 var BASE_FONTS = { base: 14, sm: 12, lg: 16, xl: 18, xxl: 24 }; 54 var BASE_SPACING = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }; 55 56 // Load current settings 57 var appearance = window.__CMS_APPEARANCE || {}; 58 var savedColors = appearance.colors || {}; 59 var state = { 60 theme: appearance.theme || '', 61 fontScale: appearance.fontScale || 1, 62 spacingScale: appearance.spacingScale || 1, 63 fontFamily: appearance.fontFamily || 'system', 64 colors: {}, 65 menuStyles: appearance.menuStyles || {}, 66 categoryStyles: appearance.categoryStyles || {} 67 }; 68 var dk = Object.keys(DEFAULTS); 69 dk.forEach(function(k) { if (savedColors[k]) state.colors[k] = savedColors[k]; }); 70 71 // Style controls available per category key 72 var STYLE_CONTROLS = { 73 heading: ['fontWeight', 'fontSize', 'textTransform'], 74 text: ['fontWeight', 'fontSize', 'textTransform'], 75 link: ['fontWeight', 'fontSize', 'textTransform'], 76 accent: ['borderRadius', 'textTransform'], 77 border: ['textTransform'], 78 menuItem: ['fontWeight', 'fontSize', 'textTransform'], 79 submenuItem: ['fontWeight', 'textTransform'], 80 navBg: ['textTransform'], 81 dropdownBg: ['textTransform'], 82 footerBg: ['textTransform'] 83 }; 84 85 var STYLE_LABELS = { 86 fontWeight: 'Weight', 87 fontSize: 'Size', 88 textTransform: 'Case', 89 borderRadius: 'Corners' 90 }; 91 92 // CSS selectors per category for applying styles 93 var CATEGORY_CSS = { 94 heading: ':root h1,:root h2,:root h3,:root h4,:root h5,:root h6,:root .pageheading,:root .layout_pageheading', 95 text: ':root p,:root li,:root td,:root dd,:root span,:root .pagebody,:root .layout_pagebody', 96 link: ':root a:not(.nav-link):not(.navbar-brand):not(.dropdown-item)', 97 navBg: ':root #qm0>a,:root #qm1>a,:root .qmmc>a,:root .nav-link,:root .navbar-brand', 98 footerBg: ':root .cus-submenu-footer,:root .cus-submenu-footer a,:root .cus-submenu-footer p,:root .cus-submenu-footer span', 99 accent: ':root .btn-primary,:root .btn-primary:hover,:root .read-more,:root .badge-primary,:root .cus-scroll-top' 100 }; 101 102 // === CATEGORY MAP === 103 104 var CATEGORY_MAP = [ 105 { key: 'menuItem', label: 'Menu Item', color: '#7c3aed', 106 controls: [], // dynamic - handled by detectMenuItem 107 match: function(el) { return !!(el.closest('[data-cms-menu-id]') || el.closest('.menubodyhorizontal') || el.closest('.menubodydefault')); } }, 108 { key: 'submenuItem', label: 'Submenu Item', color: '#a78bfa', 109 controls: [], 110 match: function(el) { return !!(el.closest('[data-cms-submenu-id]') || el.closest('.submenubodyhorizontal') || el.closest('.submenubodydefault')); } }, 111 { key: 'navBg', label: 'Navbar', color: '#8b5cf6', 112 controls: ['navBg', 'navText'], 113 match: function(el) { return !!(el.closest('#qm0,#qm1,.qmmc,.navbar,.cus-submenu-header,.column6')); } }, 114 { key: 'dropdownBg', label: 'Dropdown', color: '#6366f1', 115 controls: ['dropdownBg', 'dropdownText'], 116 match: function(el) { return !!(el.closest('.dropdown-menu,.SubMenuContainer1')); } }, 117 { key: 'footerBg', label: 'Footer', color: '#f97316', 118 controls: ['footerBg', 'footerText'], 119 match: function(el) { return !!(el.closest('.cus-submenu-footer')); } }, 120 { key: 'heading', label: 'Headings', color: '#ec4899', 121 controls: ['heading', 'background'], 122 match: function(el) { return /^H[1-6]$/.test(el.tagName) || el.classList.contains('pageheading') || el.classList.contains('layout_pageheading'); } }, 123 { key: 'link', label: 'Links', color: '#3b82f6', 124 controls: ['link', 'background'], 125 match: function(el) { return el.tagName === 'A' && !el.classList.contains('nav-link') && !el.classList.contains('navbar-brand') && !el.classList.contains('dropdown-item'); } }, 126 { key: 'accent', label: 'Accent / Buttons', color: '#10b981', 127 controls: ['accent', 'background'], 128 match: function(el) { return el.classList.contains('btn-primary') || el.classList.contains('read-more') || el.classList.contains('badge-primary') || el.classList.contains('cus-scroll-top'); } }, 129 { key: 'border', label: 'Borders', color: '#6b7280', 130 controls: ['border'], 131 match: function(el) { return el.tagName === 'HR' || el.classList.contains('card') || el.classList.contains('border') || el.tagName === 'TABLE'; } },
132 { key: 'background', label: 'Page Background', color: '#f59e0b', 133 controls: ['background'], 134 match: function(el) { return el.tagName === 'BODY' || el.classList.contains('pagebody') || el.classList.contains('layout_pagebody'); } }, 135 { key: 'text', label: 'Body Text', color: '#64748b', 136 controls: ['text', 'background'], 137 match: function(el) { return /^(P|LI|TD|DD|SPAN|DIV)$/.test(el.tagName); } } 138 ]; 139 140 var COLOR_LABELS = { 141 text: 'Body Text', heading: 'Headings', link: 'Links', 142 background: 'Background', accent: 'Accent', border: 'Borders', 143 navBg: 'Navbar BG', navText: 'Navbar Text', 144 dropdownBg: 'Dropdown BG', dropdownText: 'Dropdown Text', 145 footerBg: 'Footer BG', footerText: 'Footer Text' 146 }; 147 148 function detectCategory(el) { 149 // Skip non-stylable elements (images, video, iframes, canvas, svg) 150 if (/^(IMG|VIDEO|IFRAME|CANVAS|SVG|PICTURE|SOURCE)$/.test(el.tagName)) return null; 151 for (var i = 0; i < CATEGORY_MAP.length; i++) { 152 if (CATEGORY_MAP[i].match(el)) return CATEGORY_MAP[i]; 153 } 154 if (el.parentElement && el.parentElement !== document.documentElement && el.parentElement !== document.body) { 155 return detectCategory(el.parentElement); 156 } 157 return null; 158 } 159 160 function detectMenuItem(el) { 161 // Check for individual menu item 162 var menuA = el.closest('[data-cms-menu-id]'); 163 var menuDiv = el.closest('.menubodyhorizontal') || el.closest('.menubodydefault'); 164 var menuEl = menuA || menuDiv; 165 if (menuEl) { 166 var id = null; 167 if (menuA) id = menuA.getAttribute('data-cms-menu-id'); 168 if (!id && menuDiv) { 169 var cls = menuDiv.className || ''; 170 var m = cls.match(/custom_menu_(\d+)/); 171 if (m) id = m[1]; 172 } 173 if (!id && menuDiv) { 174 // Try data attribute on child anchor 175 var innerA = menuDiv.querySelector('[data-cms-menu-id]'); 176 if (innerA) id = innerA.getAttribute('data-cms-menu-id'); 177 } 178 var label = (menuEl.textContent || '').trim().replace(/[\n\r]+/g, ' ').substring(0, 25); 179 return { type: 'menu', id: id, label: label, el: menuEl }; 180 } 181 // Check for submenu item 182 var subA = el.closest('[data-cms-submenu-id]'); 183 var subDiv = el.closest('.submenubodyhorizontal') || el.closest('.submenubodydefault'); 184 var subEl = subA || subDiv; 185 if (subEl) { 186 var subId = null; 187 if (subA) subId = subA.getAttribute('data-cms-submenu-id'); 188 if (!subId && subDiv) { 189 var scls = subDiv.className || ''; 190 var sm = scls.match(/custom_Submenu_(\d+)/);
191 if (sm) subId = sm[1]; 192 } 193 if (!subId && subDiv) { 194 var innerSub = subDiv.querySelector('[data-cms-submenu-id]'); 195 if (innerSub) subId = innerSub.getAttribute('data-cms-submenu-id'); 196 } 197 var subLabel = (subEl.textContent || '').trim().replace(/[\n\r]+/g, ' ').substring(0, 25); 198 return { type: 'submenu', id: subId, label: subLabel, el: subEl }; 199 } 200 return null; 201 } 202 203 // === LIVE APPLICATION (unchanged logic) === 204 205 function resolveTheme(t) { 206 if (t === 'auto' || !t) return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; 207 return t; 208 } 209 210 function applyTheme(themeName) { 211 var resolved = resolveTheme(themeName); 212 document.documentElement.setAttribute('data-theme', resolved); 213 var el = document.getElementById(PREFIX + '-overrides'); 214 if (resolved === 'light') { 215 if (el) el.textContent = ''; 216 document.body.style.filter = ''; 217 document.body.style.background = ''; 218 applyColors(); 219 return; 220 } 221 var colorEl = document.getElementById(PREFIX + '-colors'); 222 if (colorEl) colorEl.textContent = ''; 223 var upColors = document.getElementById('sys-up-admin-colors'); 224 if (upColors) upColors.textContent = ''; 225 226 var filterVal = THEME_FILTERS[resolved] || ''; 227 if (!filterVal) return; 228 document.body.style.filter = filterVal; 229 document.body.style.background = '#fff'; 230 231 var css = ''; 232 if (INVERT_THEMES[resolved]) { 233 css = 'body[style*="filter"] img,body[style*="filter"] video,body[style*="filter"] iframe,' + 234 'body[style*="filter"] canvas,body[style*="filter"] svg image,' + 235 'body[style*="filter"] [style*="background-image"]{filter:' + filterVal + ' !important}'; 236 } 237 if (!el) { el = document.createElement('style'); el.id = PREFIX + '-overrides'; document.head.appendChild(el); } 238 el.textContent = css; 239 } 240 241 function applyFontScale(scale) { 242 var r = document.documentElement.style; 243 r.setProperty('--font-size-base', (BASE_FONTS.base * scale) + 'px'); 244 r.setProperty('--font-size-sm', (BASE_FONTS.sm * scale) + 'px'); 245 r.setProperty('--font-size-lg', (BASE_FONTS.lg * scale) + 'px'); 246 r.setProperty('--font-size-xl', (BASE_FONTS.xl * scale) + 'px'); 247 r.setProperty('--font-size-xxl', (BASE_FONTS.xxl * scale) + 'px'); 248 document.documentElement.style.fontSize = (100 * scale) + '%'; 249 } 250 251 function applySpacingScale(scale) { 252 var r = document.documentElement.style; 253 r.setProperty('--spacing-xs', (BASE_SPACING.xs * scale) + 'px'); 254 r.setProperty('--spacing-sm', (BASE_SPACING.sm * scale) + 'px'); 255 r.setProperty('--spacing-md', (BASE_SPACING.md * scale) + 'px'); 256 r.setProperty('--spacing-lg', (BASE_SPACING.lg * scale) + 'px'); 257 r.setProperty('--spacing-xl', (BASE_SPACING.xl * scale) + 'px'); 258 } 259 260 function applyFontFamily(key) { 261 var css = FM[key] || FM.system; 262 var linkEl = document.getElementById(PREFIX + '-gfont'); 263 if (GF[key]) { 264 var href = 'https://fonts.googleapis.com/css2?family=' + GF[key] + '&display=swap'; 265 if (!linkEl) { linkEl = document.createElement('link'); linkEl.id = PREFIX + '-gfont'; linkEl.rel = 'stylesheet'; document.head.appendChild(linkEl); } 266 linkEl.href = href; 267 } else { 268 if (linkEl) linkEl.parentNode.removeChild(linkEl); 269 } 270 var fontEl = document.getElementById(PREFIX + '-font'); 271 if (!fontEl) { fontEl = document.createElement('style'); fontEl.id = PREFIX + '-font'; document.head.appendChild(fontEl); } 272 if (key && key !== 'system') { 273 fontEl.textContent = 'body{font-family:' + css + ' !important}'; 274 } else { 275 fontEl.textContent = ''; 276 } 277 } 278 279 // Specificity boost: :root prefix adds pseudo-class specificity (0,1,0) to every selector. 280 // :root always matches <html>, needs zero JS. :root h2 (0,1,1) beats .text-white (0,1,0). 281 var SP = ':root'; 282 283 function _buildColorCss(cc) { 284 var css = ''; 285 if (cc.text) css += SP + ' body,' + SP + ' p,' + SP + ' li,' + SP + ' td,' + SP + ' dd,' + SP + ' span,' + SP + ' div,' + SP + ' .text-dark,' + SP + ' .text-black,' + SP + ' .text-muted,' + SP + ' .text-white,' + SP + ' .pagebody,' + SP + ' .layout_pagebody,' + SP + ' .pagebody p,' + SP + ' .pagebody li,' + SP + ' .pagebody td,' + SP + ' .pagebody span{color:' + cc.text + ' !important}'; 286 if (cc.heading) css += SP + ' h1,' + SP + ' h2,' + SP + ' h3,' + SP + ' h4,' + SP + ' h5,' + SP + ' h6,' + SP + ' .pageheading,' + SP + ' .layout_pageheading{color:' + cc.heading + ' !important}'; 287 if (cc.link) css += SP + ' a:not(.nav-link):not(.navbar-brand):not(.dropdown-item){color:' + cc.link + ' !important}'; 288 if (cc.background && cc.background !== '#ffffff') css += SP + ' body,' + SP + ' .pagebody,' + SP + ' .layout_pagebody{background-color:' + cc.background + ' !important}'; 289 if (cc.navBg) css += SP + ' #qm0,' + SP + ' #qm1,' + SP + ' .qmmc,' + SP + ' .navbar,' + SP + ' .cus-submenu-header{background-color:' + cc.navBg + ' !important}'; 290 if (cc.navText) css += SP + ' #qm0>a,' + SP + ' #qm1>a,' + SP + ' .qmmc>a,' + SP + ' .nav-link,' + SP + ' .navbar-brand{color:' + cc.navText + ' !important}'; 291 var ddBg = cc.dropdownBg || cc.navBg; 292 var ddText = cc.dropdownText || cc.navText; 293 if (ddBg) css += SP + ' #qm0 div,' + SP + ' #qm1 div,' + SP + ' .qmmc div,' + SP + ' #qm0 div a,' + SP + ' #qm1 div a,' + SP + ' .qmmc div a,' + SP + ' #qm0 div td,' + SP + ' #qm1 div td,' + SP + ' .qmmc div td,' + SP + ' .navbar .dropdown-menu,' + SP + ' .dropdown-menu li{background-color:' + ddBg + ' !important}'; 294 if (ddText) css += SP + ' #qm0 div a,' + SP + ' #qm1 div a,' + SP + ' .qmmc div a,' + SP + ' .dropdown-item,' + SP + ' .dropdown-menu a,' + SP + ' .dropdown-menu li a{color:' + ddText + ' !important}'; 295 if (cc.footerBg) css += SP + ' .cus-submenu-footer{background-color:' + cc.footerBg + ' !important}'; 296 if (cc.footerText) css += SP + ' .cus-submenu-footer,' + SP + ' .cus-submenu-footer a,' + SP + ' .cus-submenu-footer p,' + SP + ' .cus-submenu-footer span,' + SP + ' .cus-submenu-footer li{color:' + cc.footerText + ' !important}'; 297 if (cc.accent) css += SP + ' .btn-primary,' + SP + ' .btn-primary:hover{background-color:' + cc.accent + ' !important;border-color:' + cc.accent + ' !important}' + SP + ' .read-more,' + SP + ' .badge-primary,' + SP + ' .page-item.active .page-link{background-color:' + cc.accent + ' !important}' + SP + ' .cus-scroll-top{background-color:' + cc.accent + ' !important}'; 298 if (cc.border) css += SP + ' hr,' + SP + ' .card,' + SP + ' .border{border-color:' + cc.border + ' !important}' + SP + ' table td,' + SP + ' table th{border-color:' + cc.border + ' !important}'; 299 return css; 300 } 301 302 function applyColors() { 303 var el = document.getElementById(PREFIX + '-colors'); 304 var css = _buildColorCss(state.colors); 305 306 if (!el) { el = document.createElement('style'); el.id = PREFIX + '-colors'; document.head.appendChild(el); } 307 el.textContent = css; 308 document.head.appendChild(el); 309 310 var upColors = document.getElementById('sys-up-admin-colors'); 311 if (upColors) upColors.textContent = ''; 312 } 313 314 function applyMenuStyles() { 315 var el = document.getElementById(PREFIX + '-menu-colors'); 316 if (!el) { el = document.createElement('style'); el.id = PREFIX + '-menu-colors'; document.head.appendChild(el); } 317 var css = ''; 318 var ms = state.menuStyles; 319 for (var key in ms) { 320 if (!ms.hasOwnProperty(key)) continue; 321 var parts = key.split('_'); 322 var type = parts[0]; 323 var id = parts.slice(1).join('_'); 324 var styles = ms[key]; 325 // Build extra style props (fontWeight, fontSize, textTransform) 326 var extra = ''; 327 if (styles.fontWeight && styles.fontWeight !== 'normal') extra += 'font-weight:' + styles.fontWeight + ' !important;'; 328 if (styles.fontSize) extra += 'font-size:' + styles.fontSize + 'em !important;'; 329 if (styles.textTransform) extra += 'text-transform:' + styles.textTransform + ' !important;'; 330 331 if (type === 'menu') { 332 var mSel = '[data-cms-menu-id="' + id + '"],.custom_menu_' + id + ',.custom_menu_' + id + '>a'; 333 if (styles.bg) css += mSel + '{background-color:' + styles.bg + ' !important}'; 334 if (styles.text) css += mSel + '{color:' + styles.text + ' !important}'; 335 if (extra) css += mSel + '{' + extra + '}'; 336 } else if (type === 'submenu') { 337 var sSel = '[data-cms-submenu-id="' + id + '"],.custom_Submenu_' + id + ',.custom_Submenu_' + id + ' a'; 338 if (styles.bg) css += sSel + '{background-color:' + styles.bg + ' !important}'; 339 if (styles.text) css += sSel + '{color:' + styles.text + ' !important}'; 340 if (extra) css += sSel + '{' + extra + '}'; 341 } 342 } 343 el.textContent = css; 344 document.head.appendChild(el); 345 } 346 347 function applyCategoryStyles() { 348 var el = document.getElementById(PREFIX + '-cat-styles'); 349 if (!el) { el = document.createElement('style'); el.id = PREFIX + '-cat-styles'; document.head.appendChild(el); } 350 var css = ''; 351 var cs = state.categoryStyles; 352 for (var catKey in cs) { 353 if (!cs.hasOwnProperty(catKey) || !CATEGORY_CSS[catKey]) continue; 354 var styles = cs[catKey]; 355 var props = ''; 356 if (styles.fontWeight && styles.fontWeight !== 'normal') props += 'font-weight:' + styles.fontWeight + ' !important;'; 357 if (styles.fontSize) props += 'font-size:' + styles.fontSize + 'em !important;'; 358 if (styles.textTransform) props += 'text-transform:' + styles.textTransform + ' !important;'; 359 if (styles.borderRadius !== undefined && styles.borderRadius !== null && styles.borderRadius !== '') props += 'border-radius:' + styles.borderRadius + 'px !important;'; 360 if (props) css += CATEGORY_CSS[catKey] + '{' + props + '}'; 361 } 362 el.textContent = css; 363 document.head.appendChild(el); 364 } 365 366 function applyAll() { 367 applyTheme(state.theme); 368 applyFontScale(state.fontScale); 369 applySpacingScale(state.spacingScale); 370 applyFontFamily(state.fontFamily); 371 applyMenuStyles(); 372 applyCategoryStyles(); 373 } 374 375 // === UI STATE === 376 377 var designMode = false; 378 var fabEl, topBarEl, highlightEl, popoverEl, toastEl; 379 var _highlightTarget = null; 380 var _popoverTarget = null; 381 var _popoverCategory = null; 382 var _popoverMenuItem = null; 383 var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 384 var transition = reducedMotion ? 'none' : 'all 0.25s ease'; 385 var _rafId = null; 386 387 // === INJECT STYLES === 388 389 function injectStyles() { 390 var s = document.createElement('style'); 391 s.id = PREFIX + '-css'; 392 s.textContent = 393 // FAB 394 '@keyframes ' + PREFIX + '-pulse{0%,100%{box-shadow:0 0 8px rgba(234,88,12,0.4)}50%{box-shadow:0 0 20px rgba(234,88,12,0.8),0 0 40px rgba(234,88,12,0.3)}}' + 395 '#' + PREFIX + '-fab{position:fixed;top:16px;right:16px;width:44px;height:44px;border-radius:50%;' + 396 'background:#ea580c;color:#fff;border:none;cursor:pointer;z-index:10000;' + 397 'display:flex;align-items:center;justify-content:center;font-size:18px;' + 398 'animation:' + PREFIX + '-pulse 2s ease-in-out infinite;transition:' + transition + ';outline:none}' + 399 '#' + PREFIX + '-fab:hover{transform:scale(1.1);background:#c2410c}' + 400 '#cms-lb-hdr #' + PREFIX + '-fab{position:static;width:auto;height:auto;border-radius:6px;' + 401 'background:transparent;font-size:13px;animation:none;padding:8px 14px;gap:6px;' + 402 'color:#e2e8f0 !important;font-family:inherit;white-space:nowrap}' + 403 '#cms-lb-hdr #' + PREFIX + '-fab:hover{transform:none;background:rgba(255,255,255,0.15);color:#fff !important}' + 404 405 // Top Bar 406 '#' + PREFIX + '-topbar{position:fixed;top:-60px;left:0;width:100%;height:56px;' + 407 'background:linear-gradient(135deg,#c2410c 0%,#ea580c 50%,#f97316 100%);color:#fff;z-index:99994;display:flex;align-items:center;' + 408 'justify-content:space-between;padding:0 12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' + 409 'font-size:13px;box-shadow:0 2px 12px rgba(0,0,0,0.3);transition:top 0.3s ease}' + 410 '#' + PREFIX + '-topbar *{color:#fff !important;font-family:inherit !important}' + 411 '#' + PREFIX + '-topbar.open{top:0}' + 412 '.' + PREFIX + '-tb-left{display:flex;align-items:center;gap:8px;flex-shrink:0}' + 413 '.' + PREFIX + '-tb-left i{font-size:16px;color:#fff !important}' + 414 '.' + PREFIX + '-tb-label{font-weight:700;font-size:13px;white-space:nowrap}' + 415 '.' + PREFIX + '-tb-dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:' + PREFIX + '-pulse-dot 1.5s ease-in-out infinite}' + 416 '@keyframes ' + PREFIX + '-pulse-dot{0%,100%{opacity:1}50%{opacity:0.4}}' + 417 '.' + PREFIX + '-tb-center{display:flex;align-items:center;gap:6px;flex:1;justify-content:center;overflow-x:auto}' +
418 '.' + PREFIX + '-tb-group{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,0.15);border-radius:6px;padding:2px}' + 419 '.' + PREFIX + '-tb-group-label{font-size:9px;text-transform:uppercase;letter-spacing:0.05em;color:rgba(255,255,255,0.7) !important;padding:0 4px;white-space:nowrap}' + 420 '.' + PREFIX + '-tb-btn{background:none;border:none;color:rgba(255,255,255,0.85) !important;cursor:pointer;padding:5px 8px;border-radius:4px;font-size:12px;white-space:nowrap;outline:none}' + 421 '.' + PREFIX + '-tb-btn:hover{background:rgba(255,255,255,0.2)}' + 422 '.' + PREFIX + '-tb-btn.active{background:rgba(0,0,0,0.3) !important;color:#fff !important;font-weight:700}' + 423 '.' + PREFIX + '-tb-select{background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);color:#fff !important;border-radius:4px;' + 424 'padding:4px 6px;font-size:11px;cursor:pointer;outline:none;max-width:120px}' + 425 '.' + PREFIX + '-tb-right{display:flex;align-items:center;gap:6px;flex-shrink:0}' + 426 '.' + PREFIX + '-tb-save{background:rgba(0,0,0,0.3) !important;color:#fff !important;border:1px solid rgba(255,255,255,0.4);border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;cursor:pointer}' + 427 '.' + PREFIX + '-tb-save:hover{background:rgba(0,0,0,0.45) !important;border-color:#fff}' + 428 '.' + PREFIX + '-tb-save:disabled{opacity:0.5;cursor:not-allowed}' + 429 '.' + PREFIX + '-tb-reset{background:transparent;border:1px solid rgba(255,255,255,0.4);color:#fff !important;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer}' + 430 '.' + PREFIX + '-tb-reset:hover{background:rgba(255,255,255,0.15);border-color:#fff}' + 431 '.' + PREFIX + '-tb-exit{background:none;border:none;color:rgba(255,255,255,0.7) !important;cursor:pointer;font-size:20px;padding:4px 8px;line-height:1;border-radius:4px}' + 432 '.' + PREFIX + '-tb-exit:hover{background:rgba(255,255,255,0.15);color:#fff !important}' + 433 434 // Highlight 435 '#' + PREFIX + '-highlight{position:fixed;pointer-events:none;z-index:99990;border:2px solid transparent;' + 436 'border-radius:2px;background:rgba(37,99,235,0.04);transition:all 0.12s ease;display:none}' + 437 '#' + PREFIX + '-highlight.visible{display:block}' + 438 '.' + PREFIX + '-cat-badge{position:absolute;top:-22px;left:0;color:#fff !important;font-size:10px;padding:2px 8px;' + 439 'border-radius:3px;white-space:nowrap;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}' + 440 441 // Popover 442 '#' + PREFIX + '-popover{position:fixed;z-index:99995;width:280px;background:#fff;border-radius:10px;' + 443 'box-shadow:0 8px 30px rgba(0,0,0,0.18),0 2px 8px rgba(0,0,0,0.1);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' + 444 'font-size:13px;opacity:0;transform:translateY(6px);pointer-events:none;transition:opacity 0.2s,transform 0.2s}' + 445 '#' + PREFIX + '-popover *{color:#1f2937 !important;font-family:inherit !important}' + 446 '#' + PREFIX + '-popover.open{opacity:1;transform:translateY(0);pointer-events:auto}' + 447 '.' + PREFIX + '-pop-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #e5e7eb}' + 448 '.' + PREFIX + '-pop-title{display:flex;align-items:center;gap:6px;font-weight:700;font-size:13px}' + 449 '.' + PREFIX + '-pop-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}' + 450 '.' + PREFIX + '-pop-close{background:none;border:none;font-size:18px;cursor:pointer;color:#9ca3af !important;padding:2px 6px;border-radius:4px;line-height:1}' + 451 '.' + PREFIX + '-pop-close:hover{background:#f3f4f6;color:#374151 !important}' + 452 '.' + PREFIX + '-pop-body{padding:10px 14px}' + 453 '.' + PREFIX + '-pop-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}' + 454 '.' + PREFIX + '-pop-row:last-child{margin-bottom:0}' + 455 '.' + PREFIX + '-pop-row label{font-size:12px;font-weight:500;color:#374151 !important;white-space:nowrap;min-width:60px}' + 456 '.' + PREFIX + '-pop-row input[type="color"]{width:32px;height:26px;border:2px solid #e5e7eb;border-radius:5px;padding:1px;cursor:pointer;background:#fff}' + 457 '.' + PREFIX + '-pop-row .hex{font-size:11px;color:#6b7280 !important;font-family:monospace !important}' + 458 '.' + PREFIX + '-pop-opts{display:flex;gap:3px;flex:1}' + 459 '.' + PREFIX + '-pop-opt{padding:3px 8px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:11px;color:#374151 !important;white-space:nowrap}' + 460 '.' + PREFIX + '-pop-opt:hover{background:#f3f4f6;border-color:#9ca3af}' + 461 '.' + PREFIX + '-pop-opt.active{background:#ea580c !important;border-color:#ea580c;color:#fff !important;font-weight:600}' + 462 '.' + PREFIX + '-pop-sel{flex:1;padding:3px 6px;border:1px solid #d1d5db;border-radius:4px;font-size:11px;color:#374151 !important;background:#fff;cursor:pointer}' + 463 '.' + PREFIX + '-pop-note{font-size:11px;color:#9ca3af !important;padding:6px 0 0}' + 464 465 // Toast 466 '.' + PREFIX + '-toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;' + 467 'background:#065f46;color:#fff !important;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' + 468 'opacity:0;transition:opacity .3s;z-index:99996;pointer-events:none}' + 469 '.' + PREFIX + '-toast.show{opacity:1}' + 470 471 // Mobile 472 '@media(max-width:600px){' + 473 '#' + PREFIX + '-topbar{height:auto;flex-wrap:wrap;padding:6px 8px;gap:6px}' + 474 '.' + PREFIX + '-tb-center{display:none}' + 475 '#' + PREFIX + '-popover{position:fixed;bottom:0;left:0;right:0;top:auto !important;width:100%;' + 476 'border-radius:12px 12px 0 0;max-height:50vh;overflow-y:auto;transform:translateY(100%)}' + 477 '#' + PREFIX + '-popover.open{transform:translateY(0)}' + 478 '}'; 479 document.head.appendChild(s); 480 } 481 482 // === CREATE UI ELEMENTS === 483 484 function createFAB() { 485 fabEl = document.createElement('button'); 486 fabEl.id = PREFIX + '-fab'; 487 fabEl.setAttribute('aria-label', 'Site Appearance'); 488 fabEl.setAttribute('title', 'Site Appearance (Admin)'); 489 fabEl.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>'; 490 fabEl.addEventListener('click', toggleDesignMode); 491 document.documentElement.appendChild(fabEl); 492 _tryMoveToBar(); 493 } 494 495 function _insertAfterDragDrop(container) { 496 var ddBtn = container.querySelector('#sys-bl-fab'); 497 if (ddBtn && ddBtn.nextSibling) { 498 container.insertBefore(fabEl, ddBtn.nextSibling); 499 } else if (ddBtn) { 500 container.appendChild(fabEl); 501 } else { 502 container.insertBefore(fabEl, container.firstChild); 503 } 504 } 505 506 function _tryMoveToBar() { 507 var lnks = document.querySelector('#cms-lb-hdr .lnks'); 508 if (lnks && fabEl && fabEl.parentNode !== lnks) {
509 fabEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:middle;margin-right:4px"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>Appearance'; 510 fabEl.setAttribute('title', 'Site Appearance'); 511 _insertAfterDragDrop(lnks); 512 return; 513 } 514 if (!lnks) { 515 var obs = new MutationObserver(function(muts, o) { 516 var bar = document.querySelector('#cms-lb-hdr .lnks'); 517 if (bar && fabEl) { 518 fabEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:middle;margin-right:4px"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>Appearance'; 519 fabEl.setAttribute('title', 'Site Appearance'); 520 _insertAfterDragDrop(bar); 521 o.disconnect(); 522 } 523 }); 524 obs.observe(document.documentElement, { childList: true, subtree: true }); 525 setTimeout(function() { obs.disconnect(); }, 10000); 526 } 527 } 528 529 function createTopBar() { 530 topBarEl = document.createElement('div'); 531 topBarEl.id = PREFIX + '-topbar'; 532 533 var html = ''; 534 535 // Left section 536 html += '<div class="' + PREFIX + '-tb-left">'; 537 html += '<i class="fa fa-pen"></i>'; 538 html += '<span class="' + PREFIX + '-tb-label">Styling Mode</span>'; 539 html += '<span class="' + PREFIX + '-tb-dot"></span>'; 540 html += '</div>'; 541 542 // Center section - theme, font size, font family 543 html += '<div class="' + PREFIX + '-tb-center">'; 544 545 // Theme dropdown 546 html += '<span class="' + PREFIX + '-tb-group-label">Mode</span>'; 547 html += '<select class="' + PREFIX + '-tb-select" id="' + PREFIX + '-tb-theme">'; 548 var themes = [ 549 { key: '', label: 'Auto' }, 550 { key: 'light', label: 'Light' }, 551 { key: 'dark', label: 'Dark' }, 552 { key: 'sepia', label: 'Sepia' }, 553 { key: 'highContrast', label: 'Contrast' } 554 ]; 555 for (var i = 0; i < themes.length; i++) { 556 var t = themes[i]; 557 var tSel = state.theme === t.key ? ' selected' : ''; 558 html += '<option value="' + t.key + '"' + tSel + '>' + t.label + '</option>'; 559 } 560 html += '</select>'; 561 562 // Font size group 563 html += '<div class="' + PREFIX + '-tb-group">'; 564 html += '<span class="' + PREFIX + '-tb-group-label">Size</span>'; 565 var sizes = [ 566 { key: 0.85, label: 'S' }, 567 { key: 1, label: 'M' }, 568 { key: 1.15, label: 'L' }, 569 { key: 1.3, label: 'XL' } 570 ]; 571 for (var j = 0; j < sizes.length; j++) { 572 var sz = sizes[j]; 573 var fAct = state.fontScale === sz.key ? ' active' : ''; 574 html += '<button class="' + PREFIX + '-tb-btn' + fAct + '" data-act="fontScale" data-val="' + sz.key + '">' + sz.label + '</button>'; 575 } 576 html += '</div>'; 577 578 // Font family dropdown 579 html += '<select class="' + PREFIX + '-tb-select" id="' + PREFIX + '-tb-font">'; 580 var fonts = [ 581 { key: 'system', label: 'System' }, { key: 'arial', label: 'Arial' }, 582 { key: 'georgia', label: 'Georgia' }, { key: 'times', label: 'Times' }, 583 { key: 'verdana', label: 'Verdana' }, { key: 'roboto', label: 'Roboto' }, 584 { key: 'opensans', label: 'Open Sans' }, { key: 'lato', label: 'Lato' }, 585 { key: 'montserrat', label: 'Montserrat' }, { key: 'poppins', label: 'Poppins' }, 586 { key: 'raleway', label: 'Raleway' }, { key: 'nunito', label: 'Nunito' }, 587 { key: 'merriweather', label: 'Merriweather' }, { key: 'playfair', label: 'Playfair' } 588 ]; 589 for (var f = 0; f < fonts.length; f++) { 590 var ff = fonts[f]; 591 var sel = state.fontFamily === ff.key ? ' selected' : ''; 592 html += '<option value="' + ff.key + '"' + sel + '>' + ff.label + '</option>'; 593 } 594 html += '</select>'; 595 596 html += '</div>'; 597 598 // Right section 599 html += '<div class="' + PREFIX + '-tb-right">'; 600 html += '<button class="' + PREFIX + '-tb-save" id="' + PREFIX + '-tb-save">Save</button>'; 601 html += '<button class="' + PREFIX + '-tb-reset" id="' + PREFIX + '-tb-reset">Reset</button>'; 602 html += '<button class="' + PREFIX + '-tb-exit" id="' + PREFIX + '-tb-exit" title="Exit Styling Mode">×</button>'; 603 html += '</div>'; 604 605 topBarEl.innerHTML = html; 606 document.documentElement.appendChild(topBarEl); 607 608 // Event: font size buttons 609 topBarEl.addEventListener('click', function(e) { 610 var btn = e.target.closest('[data-act]'); 611 if (!btn) return; 612 var act = btn.getAttribute('data-act'); 613 var val = btn.getAttribute('data-val'); 614 615 if (act === 'fontScale') { 616 state.fontScale = parseFloat(val); 617 applyFontScale(state.fontScale); 618 } 619 620 var btns = topBarEl.querySelectorAll('[data-act="' + act + '"]'); 621 for (var i = 0; i < btns.length; i++) { 622 btns[i].classList.toggle('active', btns[i].getAttribute('data-val') === val); 623 } 624 });
625 626 // Event: theme dropdown 627 topBarEl.querySelector('#' + PREFIX + '-tb-theme').addEventListener('change', function() { 628 state.theme = this.value; 629 applyTheme(this.value); 630 }); 631 632 // Event: font family dropdown 633 topBarEl.querySelector('#' + PREFIX + '-tb-font').addEventListener('change', function() { 634 state.fontFamily = this.value; 635 applyFontFamily(this.value); 636 }); 637 638 // Event: Save 639 topBarEl.querySelector('#' + PREFIX + '-tb-save').addEventListener('click', doSave); 640 641 // Event: Reset 642 topBarEl.querySelector('#' + PREFIX + '-tb-reset').addEventListener('click', doReset); 643 644 // Event: Exit 645 topBarEl.querySelector('#' + PREFIX + '-tb-exit').addEventListener('click', exitDesignMode); 646 } 647 648 function createHighlight() { 649 highlightEl = document.createElement('div'); 650 highlightEl.id = PREFIX + '-highlight'; 651 highlightEl.innerHTML = '<span class="' + PREFIX + '-cat-badge"></span>'; 652 document.documentElement.appendChild(highlightEl); 653 } 654 655 function createPopover() { 656 popoverEl = document.createElement('div'); 657 popoverEl.id = PREFIX + '-popover'; 658 document.documentElement.appendChild(popoverEl); 659 660 // Event delegation for color inputs inside popover 661 popoverEl.addEventListener('input', function(e) { 662 if (e.target.type !== 'color') return; 663 var key = e.target.getAttribute('data-color'); 664 var menuKey = e.target.getAttribute('data-menu-key'); 665 var menuProp = e.target.getAttribute('data-menu-prop'); 666 if (menuKey && menuProp) { 667 // Per-menu-item color 668 if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {}; 669 state.menuStyles[menuKey][menuProp] = e.target.value; 670 var hex = popoverEl.querySelector('.hex[data-for="' + menuKey + '-' + menuProp + '"]'); 671 if (hex) hex.textContent = e.target.value; 672 applyMenuStyles(); 673 } else if (key) { 674 // Category color 675 state.colors[key] = e.target.value; 676 var hexEl = popoverEl.querySelector('.hex[data-for="' + key + '"]'); 677 if (hexEl) hexEl.textContent = e.target.value; 678 applyColors(); 679 } 680 }); 681 682 // Style option buttons (fontWeight, textTransform) 683 popoverEl.addEventListener('click', function(e) { 684 // Close button 685 if (e.target.closest('.' + PREFIX + '-pop-close')) { 686 hidePopover(); 687 return; 688 } 689 var opt = e.target.closest('.' + PREFIX + '-pop-opt'); 690 if (!opt) return; 691 var prop = opt.getAttribute('data-style-prop'); 692 var val = opt.getAttribute('data-style-val'); 693 var catKey = opt.getAttribute('data-style-cat'); 694 var menuKey = opt.getAttribute('data-style-menu'); 695 696 if (menuKey) { 697 if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {}; 698 state.menuStyles[menuKey][prop] = val || null; 699 applyMenuStyles(); 700 } else if (catKey) { 701 if (!state.categoryStyles[catKey]) state.categoryStyles[catKey] = {}; 702 state.categoryStyles[catKey][prop] = val || null; 703 applyCategoryStyles(); 704 } 705 // Update active state for sibling buttons 706 var siblings = opt.parentNode.querySelectorAll('.' + PREFIX + '-pop-opt'); 707 for (var i = 0; i < siblings.length; i++) siblings[i].classList.remove('active'); 708 opt.classList.add('active'); 709 }); 710 711 // Style select dropdowns (fontSize) 712 popoverEl.addEventListener('change', function(e) { 713 var sel = e.target; 714 if (!sel.hasAttribute('data-style-prop')) return; 715 var prop = sel.getAttribute('data-style-prop'); 716 var val = sel.value; 717 var catKey = sel.getAttribute('data-style-cat'); 718 var menuKey = sel.getAttribute('data-style-menu'); 719 720 if (menuKey) { 721 if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {}; 722 state.menuStyles[menuKey][prop] = val || null; 723 applyMenuStyles(); 724 } else if (catKey) { 725 if (!state.categoryStyles[catKey]) state.categoryStyles[catKey] = {}; 726 state.categoryStyles[catKey][prop] = val || null; 727 applyCategoryStyles(); 728 } 729 });
730 } 731 732 function createToast() { 733 toastEl = document.createElement('div'); 734 toastEl.className = PREFIX + '-toast'; 735 toastEl.id = PREFIX + '-toast'; 736 document.documentElement.appendChild(toastEl); 737 } 738 739 // === DESIGN MODE === 740 741 function toggleDesignMode() { 742 if (designMode) exitDesignMode(); else enterDesignMode(); 743 } 744 745 function enterDesignMode() { 746 designMode = true; 747 topBarEl.classList.add('open'); 748 749 // Push page content down 750 var barOffset = _getBarOffset(); 751 document.body.style.paddingTop = (barOffset + 56) + 'px'; 752 document.body.style.transition = 'padding-top 0.3s ease'; 753 754 // Attach event listeners (capture phase) 755 document.addEventListener('mouseover', onDesignHover, true); 756 document.addEventListener('mouseout', onDesignHoverOut, true); 757 document.addEventListener('click', onDesignClick, true); 758 document.addEventListener('keydown', onDesignKeydown); 759 window.addEventListener('scroll', onScrollOrResize, { passive: true }); 760 window.addEventListener('resize', onScrollOrResize, { passive: true }); 761 762 // Hide FAB 763 fabEl.style.display = 'none'; 764 765 // Hide all Live Editor elements while Styling Mode is active 766 if (!document.getElementById(PREFIX + '-hide-lb')) { 767 var lbHide = document.createElement('style'); 768 lbHide.id = PREFIX + '-hide-lb'; 769 lbHide.textContent = '#cms-lb-hdr,[class^="cms-lb-ov"],[class*=" cms-lb-ov"],[class^="cms-lb-mi"],[class*=" cms-lb-mi"],[class^="cms-lb-ad"],[class*=" cms-lb-ad"],[class^="cms-lb-el"],[class*=" cms-lb-el"]{display:none !important}'; 770 document.head.appendChild(lbHide); 771 } 772 } 773 774 function exitDesignMode() { 775 designMode = false; 776 topBarEl.classList.remove('open'); 777 document.body.style.paddingTop = ''; 778 document.body.style.transition = ''; 779 780 document.removeEventListener('mouseover', onDesignHover, true); 781 document.removeEventListener('mouseout', onDesignHoverOut, true); 782 document.removeEventListener('click', onDesignClick, true); 783 document.removeEventListener('keydown', onDesignKeydown); 784 window.removeEventListener('scroll', onScrollOrResize); 785 window.removeEventListener('resize', onScrollOrResize); 786 787 hideHighlight(); 788 hidePopover(); 789 790 fabEl.style.display = ''; 791 792 // Restore Live Editor elements 793 var lbHide = document.getElementById(PREFIX + '-hide-lb'); 794 if (lbHide) lbHide.remove(); 795 } 796 797 function _getBarOffset() { 798 var liveBar = document.getElementById('cms-lb-hdr'); 799 if (liveBar) return liveBar.offsetHeight || 0; 800 return 0; 801 } 802 803 // === HOVER HIGHLIGHT === 804 805 var _hoverTick = false; 806 807 function onDesignHover(e) { 808 var el = e.target; 809 if (el.closest('#' + PREFIX + '-topbar,#' + PREFIX + '-popover,#' + PREFIX + '-highlight,#' + PREFIX + '-fab,#sys-up-fab,#sys-up-panel')) return; 810 811 if (_hoverTick) return; 812 _hoverTick = true; 813 requestAnimationFrame(function() { 814 _hoverTick = false; 815 var cat = detectCategory(el); 816 if (!cat) { hideHighlight(); return; } 817 showHighlight(el, cat); 818 }); 819 } 820 821 function onDesignHoverOut(e) { 822 if (e.relatedTarget && ( 823 (e.relatedTarget.closest && e.relatedTarget.closest('#' + PREFIX + '-highlight')) || 824 e.relatedTarget === highlightEl 825 )) return; 826 // Don't hide if moving within the same element 827 if (e.relatedTarget && _highlightTarget && _highlightTarget.contains(e.relatedTarget)) return; 828 hideHighlight(); 829 } 830 831 function showHighlight(el, cat) { 832 // For menu/submenu items, get the actual container element 833 var targetEl = el; 834 if (cat.key === 'menuItem' || cat.key === 'submenuItem') { 835 var mi = detectMenuItem(el); 836 if (mi) targetEl = mi.el; 837 } 838 839 _highlightTarget = targetEl; 840 var rect = targetEl.getBoundingClientRect(); 841 var pad = 2; 842 843 highlightEl.style.top = (rect.top - pad) + 'px'; 844 highlightEl.style.left = (rect.left - pad) + 'px'; 845 highlightEl.style.width = (rect.width + pad * 2) + 'px'; 846 highlightEl.style.height = (rect.height + pad * 2) + 'px'; 847 highlightEl.style.borderColor = cat.color; 848 849 var badge = highlightEl.querySelector('.' + PREFIX + '-cat-badge'); 850 var label = cat.label; 851 if (cat.key === 'menuItem' || cat.key === 'submenuItem') { 852 var mi2 = detectMenuItem(el); 853 if (mi2) label = (mi2.type === 'menu' ? 'Menu: ' : 'Submenu: ') + mi2.label; 854 } 855 badge.textContent = label; 856 badge.style.background = cat.color; 857 858 highlightEl.classList.add('visible'); 859 } 860 861 function hideHighlight() { 862 highlightEl.classList.remove('visible'); 863 _highlightTarget = null; 864 } 865 866 function repositionHighlight() { 867 if (!_highlightTarget) return; 868 var rect = _highlightTarget.getBoundingClientRect(); 869 var pad = 2; 870 highlightEl.style.top = (rect.top - pad) + 'px'; 871 highlightEl.style.left = (rect.left - pad) + 'px'; 872 highlightEl.style.width = (rect.width + pad * 2) + 'px'; 873 highlightEl.style.height = (rect.height + pad * 2) + 'px'; 874 } 875 876 // === CLICK POPOVER === 877 878 function onDesignClick(e) { 879 var el = e.target; 880 // Allow clicks on our own UI 881 if (el.closest('#' + PREFIX + '-topbar,#' + PREFIX + '-popover,#sys-up-fab,#sys-up-panel')) return; 882 883 e.preventDefault(); 884 e.stopPropagation(); 885
886 var cat = detectCategory(el); 887 if (!cat) { hidePopover(); return; } 888 889 // For menu/submenu items, show per-item popover 890 if (cat.key === 'menuItem' || cat.key === 'submenuItem') { 891 var mi = detectMenuItem(el); 892 if (mi && mi.id) { 893 showMenuPopover(mi, cat); 894 return; 895 } 896 } 897 898 showCategoryPopover(el, cat); 899 } 900 901 function showCategoryPopover(el, cat) { 902 _popoverTarget = el; 903 _popoverCategory = cat; 904 _popoverMenuItem = null; 905 906 // Only disable color pickers when admin explicitly chose a non-light theme (not Auto) 907 var isExplicitNonLight = state.theme === 'dark' || state.theme === 'sepia' || state.theme === 'highContrast'; 908 909 var html = ''; 910 html += '<div class="' + PREFIX + '-pop-hdr">'; 911 html += '<div class="' + PREFIX + '-pop-title"><span class="' + PREFIX + '-pop-dot" style="background:' + cat.color + '"></span>' + cat.label + '</div>'; 912 html += '<button class="' + PREFIX + '-pop-close">×</button>'; 913 html += '</div>'; 914 html += '<div class="' + PREFIX + '-pop-body">'; 915 916 if (isExplicitNonLight && cat.controls.length > 0) { 917 html += '<div class="' + PREFIX + '-pop-note">Color pickers disabled in ' + state.theme + ' theme. Switch to Light or Auto to edit colors.</div>'; 918 } else { 919 for (var i = 0; i < cat.controls.length; i++) { 920 var key = cat.controls[i]; 921 var val = state.colors[key] || DEFAULTS[key]; 922 var lbl = COLOR_LABELS[key] || key; 923 html += '<div class="' + PREFIX + '-pop-row">'; 924 html += '<label>' + lbl + '</label>'; 925 html += '<input type="color" data-color="' + key + '" value="' + val + '">'; 926 html += '<span class="hex" data-for="' + key + '">' + val + '</span>'; 927 html += '</div>'; 928 } 929 } 930 931 if (cat.controls.length === 0 && cat.key !== 'menuItem' && cat.key !== 'submenuItem' && !STYLE_CONTROLS[cat.key]) { 932 html += '<div class="' + PREFIX + '-pop-note">Click a specific element for styling options.</div>'; 933 } 934 935 // Style controls (fontWeight, fontSize, textTransform) 936 var styleKeys = STYLE_CONTROLS[cat.key]; 937 if (styleKeys) { 938 var existing = state.categoryStyles[cat.key] || {}; 939 html += _buildStyleControlsHtml(styleKeys, existing, 'data-style-cat="' + cat.key + '"'); 940 } 941 942 html += '</div>'; 943 popoverEl.innerHTML = html; 944 popoverEl.classList.add('open'); 945 repositionPopover(el); 946 } 947 948 function showMenuPopover(mi, cat) { 949 _popoverTarget = mi.el; 950 _popoverCategory = cat; 951 _popoverMenuItem = mi; 952 953 var menuKey = mi.type + '_' + mi.id; 954 var existing = state.menuStyles[menuKey] || {}; 955 956 var html = ''; 957 html += '<div class="' + PREFIX + '-pop-hdr">'; 958 html += '<div class="' + PREFIX + '-pop-title"><span class="' + PREFIX + '-pop-dot" style="background:' + cat.color + '"></span>'; 959 html += (mi.type === 'menu' ? 'Menu' : 'Submenu') + ': ' + _escHtml(mi.label) + '</div>'; 960 html += '<button class="' + PREFIX + '-pop-close">×</button>'; 961 html += '</div>'; 962 html += '<div class="' + PREFIX + '-pop-body">'; 963 964 // BG color 965 var bgVal = existing.bg || (mi.type === 'menu' ? (state.colors.navBg || DEFAULTS.navBg) : (state.colors.dropdownBg || DEFAULTS.dropdownBg)); 966 html += '<div class="' + PREFIX + '-pop-row">'; 967 html += '<label>BG Color</label>'; 968 html += '<input type="color" data-menu-key="' + menuKey + '" data-menu-prop="bg" value="' + bgVal + '">'; 969 html += '<span class="hex" data-for="' + menuKey + '-bg">' + bgVal + '</span>'; 970 html += '</div>'; 971 972 // Text color 973 var textVal = existing.text || (mi.type === 'menu' ? (state.colors.navText || DEFAULTS.navText) : (state.colors.dropdownText || DEFAULTS.dropdownText)); 974 html += '<div class="' + PREFIX + '-pop-row">'; 975 html += '<label>Text Color</label>'; 976 html += '<input type="color" data-menu-key="' + menuKey + '" data-menu-prop="text" value="' + textVal + '">'; 977 html += '<span class="hex" data-for="' + menuKey + '-text">' + textVal + '</span>'; 978 html += '</div>'; 979 980 // Style controls for menu items 981 var menuStyleKeys = STYLE_CONTROLS[cat.key] || []; 982 if (menuStyleKeys.length) { 983 html += _buildStyleControlsHtml(menuStyleKeys, existing, 'data-style-menu="' + menuKey + '"'); 984 } 985 986 // no note needed 987 html += '</div>'; 988 989 popoverEl.innerHTML = html; 990 popoverEl.classList.add('open'); 991 repositionPopover(mi.el); 992 } 993 994 function hidePopover() { 995 popoverEl.classList.remove('open'); 996 _popoverTarget = null; 997 _popoverCategory = null; 998 _popoverMenuItem = null; 999 } 1000 1001 function repositionPopover(targetEl) { 1002 if (!targetEl) return; 1003 var rect = targetEl.getBoundingClientRect(); 1004 var popW = 280; 1005 var popH = popoverEl.offsetHeight || 160; 1006 var vpW = window.innerWidth; 1007 var vpH = window.innerHeight; 1008 var margin = 8; 1009 var topBarH = 56 + _getBarOffset(); 1010 1011 // Mobile: use bottom sheet positioning (CSS handles it) 1012 if (vpW <= 600) { 1013 popoverEl.style.top = ''; 1014 popoverEl.style.left = ''; 1015 return; 1016 } 1017 1018 // Preferred: below the element 1019 var top = rect.bottom + margin; 1020 var left = rect.left + (rect.width / 2) - (popW / 2); 1021 1022 // If below overflows, place above 1023 if (top + popH > vpH - margin) { 1024 top = rect.top - popH - margin; 1025 } 1026 // If above overflows (too close to top bar), place beside 1027 if (top < topBarH + margin) { 1028 top = Math.max(topBarH + margin, rect.top); 1029 left = rect.right + margin; 1030 if (left + popW > vpW - margin) { 1031 left = rect.left - popW - margin; 1032 } 1033 } 1034 1035 left = Math.max(margin, Math.min(left, vpW - popW - margin)); 1036 top = Math.max(topBarH + margin, top); 1037 1038 popoverEl.style.top = top + 'px'; 1039 popoverEl.style.left = left + 'px'; 1040 } 1041 1042 // === SCROLL/RESIZE === 1043 1044 function onScrollOrResize() { 1045 if (_rafId) return; 1046 _rafId = requestAnimationFrame(function() { 1047 _rafId = null; 1048 if (_highlightTarget) repositionHighlight(); 1049 if (_popoverTarget) { 1050 var rect = _popoverTarget.getBoundingClientRect(); 1051 var vpH = window.innerHeight; 1052 // Hide popover if target scrolled out of view 1053 if (rect.bottom < 56 || rect.top > vpH) { 1054 hidePopover(); 1055 } else { 1056 repositionPopover(_popoverTarget); 1057 } 1058 } 1059 });
1060 } 1061 1062 // === KEYBOARD === 1063 1064 function onDesignKeydown(e) { 1065 if (e.key === 'Escape') { 1066 if (popoverEl.classList.contains('open')) { 1067 hidePopover(); 1068 } else { 1069 exitDesignMode(); 1070 } 1071 } 1072 } 1073 1074 // === SAVE / RESET === 1075 1076 function doSave() { 1077 var btn = topBarEl.querySelector('#' + PREFIX + '-tb-save'); 1078 btn.disabled = true; 1079 btn.textContent = 'Saving...'; 1080 1081 var c = state.colors; 1082 var colorsObj = null; 1083 var setKeys = dk.filter(function(k) { return !!c[k]; }); 1084 if (setKeys.length) { 1085 colorsObj = {}; 1086 setKeys.forEach(function(k) { colorsObj[k] = c[k]; }); 1087 } 1088 1089 var menuStylesObj = null; 1090 var msKeys = Object.keys(state.menuStyles); 1091 if (msKeys.length) { 1092 menuStylesObj = {}; 1093 msKeys.forEach(function(k) { menuStylesObj[k] = state.menuStyles[k]; }); 1094 } 1095 1096 var categoryStylesObj = null; 1097 var csKeys = Object.keys(state.categoryStyles); 1098 if (csKeys.length) { 1099 categoryStylesObj = {}; 1100 csKeys.forEach(function(k) { categoryStylesObj[k] = state.categoryStyles[k]; }); 1101 } 1102 1103 var body = { 1104 appearance: { 1105 theme: state.theme || null, 1106 fontScale: state.fontScale, 1107 spacingScale: state.spacingScale, 1108 fontFamily: state.fontFamily || 'system', 1109 colors: colorsObj, 1110 menuStyles: menuStylesObj, 1111 categoryStyles: categoryStylesObj 1112 } 1113 }; 1114 1115 fetch(_bp + '/api/sys-settings', { 1116 method: 'PUT', 1117 headers: { 'Content-Type': 'application/json' }, 1118 body: JSON.stringify(body) 1119 }) 1120 .then(function(r) { return r.json(); }) 1121 .then(function(j) { 1122 if (j.success) { 1123 toast('Saved!', true); 1124 try { sessionStorage.setItem(PREFIX + '-refresh', String(Date.now())); } catch(e) {} 1125 setTimeout(function() { _hotReloadAssets(); }, 3000); 1126 } else { 1127 toast('Error: ' + (j.error || 'Save failed'), false); 1128 } 1129 btn.disabled = false; 1130 btn.textContent = 'Save'; 1131 }) 1132 .catch(function(e) { 1133 toast('Error: ' + e.message, false); 1134 btn.disabled = false; 1135 btn.textContent = 'Save'; 1136 }); 1137 } 1138 1139 function doReset() { 1140 var resetBtn = topBarEl.querySelector('#' + PREFIX + '-tb-reset'); 1141 resetBtn.textContent = 'Resetting...'; 1142 1143 // Reset state 1144 state.theme = ''; 1145 state.fontScale = 1; 1146 state.spacingScale = 1; 1147 state.fontFamily = 'system'; 1148 state.colors = {}; 1149 state.menuStyles = {}; 1150 state.categoryStyles = {}; 1151 dk.forEach(function(k) { state.colors[k] = DEFAULTS[k]; }); 1152 1153 // Update top bar active states 1154 var allBtns = topBarEl.querySelectorAll('[data-act]'); 1155 for (var i = 0; i < allBtns.length; i++) { 1156 var b = allBtns[i]; 1157 var act = b.getAttribute('data-act'); 1158 var val = b.getAttribute('data-val'); 1159 if (act === 'theme') b.classList.toggle('active', val === ''); 1160 else if (act === 'fontScale') b.classList.toggle('active', val === '1'); 1161 } 1162 1163 // Reset theme dropdown 1164 var themeSel = topBarEl.querySelector('#' + PREFIX + '-tb-theme'); 1165 if (themeSel) themeSel.value = ''; 1166 1167 // Reset font dropdown 1168 var fontSel = topBarEl.querySelector('#' + PREFIX + '-tb-font'); 1169 if (fontSel) fontSel.value = 'system'; 1170 1171 // Apply visual reset 1172 document.documentElement.setAttribute('data-theme', 'light'); 1173 document.body.style.filter = ''; 1174 document.body.style.background = ''; 1175 var overrideEl = document.getElementById(PREFIX + '-overrides'); 1176 if (overrideEl) overrideEl.textContent = ''; 1177 1178 applyFontScale(1); 1179 applySpacingScale(1); 1180 applyFontFamily('system'); 1181 1182 // Force-apply default colors using same high-specificity selectors 1183 var colorEl = document.getElementById(PREFIX + '-colors'); 1184 if (!colorEl) { colorEl = document.createElement('style'); colorEl.id = PREFIX + '-colors'; document.head.appendChild(colorEl); } 1185 colorEl.textContent = _buildColorCss(state.colors); 1186 document.head.appendChild(colorEl); 1187 1188 // Clear menu styles and category styles 1189 var menuEl = document.getElementById(PREFIX + '-menu-colors'); 1190 if (menuEl) menuEl.textContent = ''; 1191 var catStyleEl = document.getElementById(PREFIX + '-cat-styles'); 1192 if (catStyleEl) catStyleEl.textContent = ''; 1193 1194 // Clear visitor widget colors 1195 var upColors = document.getElementById('sys-up-admin-colors'); 1196 if (upColors) upColors.textContent = ''; 1197 1198 // Hide popover since everything is reset 1199 hidePopover(); 1200 1201 // Save defaults to server 1202 var saveBtn = topBarEl.querySelector('#' + PREFIX + '-tb-save'); 1203 saveBtn.disabled = true; 1204 saveBtn.textContent = 'Saving...'; 1205 1206 var body = { 1207 appearance: null, 1208 _resetAppearance: true 1209 }; 1210 1211 fetch(_bp + '/api/sys-settings', { 1212 method: 'PUT', 1213 headers: { 'Content-Type': 'application/json' }, 1214 body: JSON.stringify(body) 1215 }) 1216 .then(function(r) { return r.json(); }) 1217 .then(function(j) { 1218 if (j.success) { 1219 toast('Reset & saved! Reloadingâ¦', true); 1220 // Show spinner overlay while waiting for regeneration + reload 1221 _showResetOverlay(); 1222 // Allow background CSS/page regeneration to finish, then hard-reload 1223 setTimeout(function() { location.reload(true); }, 3500); 1224 } else { 1225 toast('Error: ' + (j.error || 'Reset failed'), false); 1226 saveBtn.disabled = false; 1227 saveBtn.textContent = 'Save'; 1228 resetBtn.textContent = 'Reset'; 1229 } 1230 }) 1231 .catch(function(e) { 1232 toast('Error: ' + e.message, false); 1233 saveBtn.disabled = false; 1234 saveBtn.textContent = 'Save'; 1235 resetBtn.textContent = 'Reset'; 1236 });
1237 } 1238 1239 // === UTILITIES === 1240 1241 function toast(msg, ok) { 1242 var t = document.getElementById(PREFIX + '-toast'); 1243 if (!t) return; 1244 t.textContent = msg; 1245 t.style.background = ok ? '#065f46' : '#991b1b'; 1246 t.classList.add('show'); 1247 setTimeout(function() { t.classList.remove('show'); }, 3000); 1248 } 1249 1250 function _buildStyleControlsHtml(controlKeys, existingStyles, dataAttr) { 1251 // dataAttr is e.g. 'data-style-cat="heading"' or 'data-style-menu="menu_5"' 1252 var html = ''; 1253 for (var i = 0; i < controlKeys.length; i++) { 1254 var prop = controlKeys[i]; 1255 var curVal = (existingStyles && existingStyles[prop]) || ''; 1256 1257 html += '<div class="' + PREFIX + '-pop-row">'; 1258 html += '<label>' + (STYLE_LABELS[prop] || prop) + '</label>'; 1259 1260 if (prop === 'fontWeight') { 1261 html += '<div class="' + PREFIX + '-pop-opts">'; 1262 html += '<button class="' + PREFIX + '-pop-opt' + (curVal !== 'bold' ? ' active' : '') + '" data-style-prop="fontWeight" data-style-val="" ' + dataAttr + '>Normal</button>'; 1263 html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'bold' ? ' active' : '') + '" data-style-prop="fontWeight" data-style-val="bold" ' + dataAttr + '>Bold</button>'; 1264 html += '</div>'; 1265 } else if (prop === 'textTransform') { 1266 html += '<div class="' + PREFIX + '-pop-opts">'; 1267 html += '<button class="' + PREFIX + '-pop-opt' + (!curVal ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="" ' + dataAttr + '>Aa</button>'; 1268 html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'uppercase' ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="uppercase" ' + dataAttr + '>AA</button>'; 1269 html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'capitalize' ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="capitalize" ' + dataAttr + '>Ab</button>'; 1270 html += '</div>'; 1271 } else if (prop === 'fontSize') { 1272 html += '<select class="' + PREFIX + '-pop-sel" data-style-prop="fontSize" ' + dataAttr + '>'; 1273 var sizes = [ 1274 { v: '', l: 'Default' }, { v: '0.85', l: 'Small' }, { v: '1', l: 'Normal' }, 1275 { v: '1.15', l: 'Large' }, { v: '1.3', l: 'X-Large' }, { v: '1.5', l: 'XX-Large' } 1276 ]; 1277 for (var s = 0; s < sizes.length; s++) { 1278 html += '<option value="' + sizes[s].v + '"' + (curVal === sizes[s].v ? ' selected' : '') + '>' + sizes[s].l + '</option>'; 1279 } 1280 html += '</select>'; 1281 } else if (prop === 'borderRadius') { 1282 html += '<div class="' + PREFIX + '-pop-opts">'; 1283 html += '<button class="' + PREFIX + '-pop-opt' + (!curVal ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="" ' + dataAttr + '>Default</button>'; 1284 html += '<button class="' + PREFIX + '-pop-opt' + (curVal === '0' ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="0" ' + dataAttr + '>Sharp</button>'; 1285 html += '<button class="' + PREFIX + '-pop-opt' + (curVal === '20' ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="20" ' + dataAttr + '>Round</button>'; 1286 html += '</div>'; 1287 } 1288 1289 html += '</div>'; 1290 } 1291 return html; 1292 } 1293 1294 function _escHtml(str) { 1295 var d = document.createElement('div'); 1296 d.textContent = str; 1297 return d.innerHTML; 1298 } 1299 1300 function _showResetOverlay() { 1301 var ov = document.createElement('div'); 1302 ov.id = PREFIX + '-reset-overlay'; 1303 ov.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,0.75);z-index:999999;display:flex;align-items:center;justify-content:center;flex-direction:column'; 1304 ov.innerHTML = '<div style="width:48px;height:48px;border:4px solid #e5e7eb;border-top-color:#ea580c;border-radius:50%;animation:' + PREFIX + '-spin 0.8s linear infinite"></div>' + 1305 '<div style="margin-top:16px;font:600 15px/1 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#374151">Resetting stylesâ¦</div>'; 1306 // inject spin keyframes if not already present 1307 if (!document.getElementById(PREFIX + '-spin-css')) { 1308 var sk = document.createElement('style'); 1309 sk.id = PREFIX + '-spin-css'; 1310 sk.textContent = '@keyframes ' + PREFIX + '-spin{to{transform:rotate(360deg)}}'; 1311 document.head.appendChild(sk); 1312 } 1313 document.body.appendChild(ov); 1314 } 1315 1316 function _hotReloadAssets() { 1317 var cb = '_v=' + Date.now(); 1318 var links = document.querySelectorAll('link[href*="custom.css"]'); 1319 for (var i = 0; i < links.length; i++) { 1320 var href = links[i].href.split('?')[0]; 1321 links[i].href = href + '?' + cb; 1322 } 1323 var sc = document.createElement('script'); 1324 sc.src = '/settings.js?' + cb; 1325 document.head.appendChild(sc); 1326 } 1327 1328 // === INIT === 1329
1330 function init() { 1331 try { 1332 var ts = sessionStorage.getItem(PREFIX + '-refresh'); 1333 if (ts && (Date.now() - parseInt(ts, 10)) < 60000) { 1334 sessionStorage.removeItem(PREFIX + '-refresh'); 1335 _hotReloadAssets(); 1336 } 1337 } catch(e) {} 1338 1339 function buildUI() { 1340 injectStyles(); 1341 createFAB(); 1342 createTopBar(); 1343 createHighlight(); 1344 createPopover(); 1345 createToast(); 1346 // Apply any saved menu/category styles on load 1347 applyMenuStyles(); 1348 applyCategoryStyles(); 1349 } 1350 if (document.readyState === 'loading') { 1351 document.addEventListener('DOMContentLoaded', buildUI); 1352 } else { 1353 buildUI(); 1354 } 1355 } 1356 1357 init(); 1358})();
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.