- Move all inline <script> blocks (layout head/theme, topbar dark toggle, foot, paste, new, history, mine, settings, admin, unlock) to external files under internal/web/static/. Page data reaches scripts via data-* attributes (data-paste-id, data-default-dark) instead of template vars. - Replace inline onclick handlers (copy, delete, stats toggle) with addEventListener wiring. - Convert inline style="" attributes to CSS utility classes; swatch colors are now set via CSSOM/DOM APIs instead of innerHTML strings. - script-src/style-src are now plain 'self'; img-src data: stays for the SVG data-URI backgrounds. Verified with headless chromium: zero CSP violations on all pages in dark and light presets, theme swatches, admin lock, tables and paste view render correctly.
102 lines
4.2 KiB
JavaScript
102 lines
4.2 KiB
JavaScript
// #112/#127/#139: theme switcher. Swatch colors are derived from computed CSS vars per preset.
|
|
(function () {
|
|
// #112: derive each preset's swatches from the real CSS variables in
|
|
// app.css by temporarily applying data-preset, so they can never drift.
|
|
// #127: 5 theme pairs, light swatches top row, dark bottom row.
|
|
var pairs = [
|
|
{ id: 'midnight', name: 'Midnight' },
|
|
{ id: 'smooth', name: 'Smooth' },
|
|
{ id: 'pastel-lavender', name: 'Pastel Lavender' },
|
|
{ id: 'pastel-peach', name: 'Pastel Peach' },
|
|
{ id: 'pastel-cloud', name: 'Pastel Cloud' }
|
|
];
|
|
var SWATCH_VARS = ['--bg', '--surface', '--surface-2', '--muted', '--accent'];
|
|
|
|
function presetColors(id) {
|
|
var root = document.documentElement;
|
|
var prev = root.getAttribute('data-preset');
|
|
root.setAttribute('data-preset', id);
|
|
var cs = getComputedStyle(root);
|
|
var colors = SWATCH_VARS.map(function (v) { return cs.getPropertyValue(v).trim(); });
|
|
if (prev === null) root.removeAttribute('data-preset'); else root.setAttribute('data-preset', prev);
|
|
return colors;
|
|
}
|
|
|
|
// current base pair + dark flag from the resolved data-preset.
|
|
// "midnight" is itself the dark variant, so check dark ids first.
|
|
function state() {
|
|
var p = document.documentElement.dataset.preset || 'midnight';
|
|
if (p === 'midnight' || /-dark$/.test(p)) {
|
|
return { base: p === 'midnight' ? 'midnight' : p.replace(/-dark$/, ''), dark: true };
|
|
}
|
|
return { base: p === 'midnight-light' ? 'midnight' : p, dark: false };
|
|
}
|
|
|
|
var grid = document.getElementById('theme-grid');
|
|
var cards = {};
|
|
// #132: midnight is dark-first (root preset = midnight = dark; its light
|
|
// variant is midnight-light), the others are light-first. Resolve the
|
|
// LIGHT and DARK preset ids generically so the light swatches always
|
|
// render in the top row of every card.
|
|
function lightPreset(id) {
|
|
if (id === 'midnight') return 'midnight-light';
|
|
return id; // light-first bases use themselves as the light variant
|
|
}
|
|
function darkPreset(id) {
|
|
if (id === 'midnight') return 'midnight'; // root preset is midnight's dark
|
|
return id + '-dark';
|
|
}
|
|
pairs.forEach(function (t) {
|
|
var light = presetColors(lightPreset(t.id));
|
|
var dark = presetColors(darkPreset(t.id));
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'theme-card';
|
|
btn.setAttribute('aria-pressed', 'false');
|
|
btn.setAttribute('data-pair', t.id);
|
|
// #139: build the swatches via DOM APIs (CSSOM styles) instead of
|
|
// innerHTML strings: re-parsing serialized inline styles would trip
|
|
// style-src 'self'.
|
|
var label = document.createElement('strong');
|
|
label.textContent = t.name;
|
|
btn.appendChild(label);
|
|
[light, dark].forEach(function (colors) {
|
|
var row = document.createElement('span');
|
|
row.className = 'swatches';
|
|
colors.forEach(function (c) {
|
|
var sw = document.createElement('span');
|
|
sw.className = 'swatch';
|
|
sw.style.backgroundColor = c;
|
|
row.appendChild(sw);
|
|
});
|
|
btn.appendChild(row);
|
|
});
|
|
btn.addEventListener('click', function () {
|
|
var dark = state().dark;
|
|
document.documentElement.dataset.preset = dark ? t.id + '-dark' : t.id;
|
|
try { localStorage.setItem('palette-theme', t.id); } catch (e) {}
|
|
Object.keys(cards).forEach(function (k) { cards[k].setAttribute('aria-pressed', 'false'); });
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
});
|
|
cards[t.id] = btn;
|
|
grid.appendChild(btn);
|
|
});
|
|
|
|
function sync() {
|
|
var s = state();
|
|
Object.keys(cards).forEach(function (k) {
|
|
cards[k].setAttribute('aria-pressed', k === s.base ? 'true' : 'false');
|
|
});
|
|
var dt = document.getElementById('settings-dark-toggle');
|
|
if (dt) dt.setAttribute('aria-pressed', s.dark ? 'true' : 'false');
|
|
}
|
|
sync();
|
|
// the topbar script runs before this button exists, so wire it here
|
|
var dt = document.getElementById('settings-dark-toggle');
|
|
dt.addEventListener('click', function () {
|
|
var btns = document.querySelectorAll('.topbar .dark-toggle');
|
|
if (btns.length) btns[0].click(); else document.dispatchEvent(new CustomEvent('palette-darkchange'));
|
|
});
|
|
document.addEventListener('palette-darkchange', sync);
|
|
})();
|