PALETTE: swatch preview + tooltip + OK commit — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

Clicking a swatch instantly swaps the body palette class for a live
preview; OK commits silently (POST, no reload); click-elsewhere or
10 s auto-dismiss reverts. Tooltip portal shows label, shoptalk,
lock state. Locked swatches show × (disabled). 20 FTs green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-18 02:05:27 -04:00
parent 6e995647e4
commit 122de3bc80
7 changed files with 488 additions and 46 deletions

View File

@@ -1,12 +1,7 @@
// console.log("apps/scripts/dashboard.js loading");
const initialize = (inputSelector) => {
// console.log("initialize called!");
const textInput = document.querySelector(inputSelector);
if (!textInput) return;
textInput.oninput = () => {
// console.log("oninput triggered");
textInput.classList.remove("is-invalid");
};
textInput.oninput = () => textInput.classList.remove("is-invalid");
};
const bindPaletteWheel = () => {
@@ -18,6 +13,125 @@ const bindPaletteWheel = () => {
});
};
// ── Palette swatch preview + commit ──────────────────────────────────────────
const bindPaletteSwatches = () => {
const portal = document.getElementById('id_tooltip_portal');
let activePreview = null;
let originalPalette = null;
let dismissTimer = null;
function currentBodyPalette() {
return [...document.body.classList].find(c => c.startsWith('palette-'));
}
function swapPalette(paletteName) {
const old = currentBodyPalette();
if (old) document.body.classList.remove(old);
document.body.classList.add(paletteName);
}
function showTooltip(swatch) {
if (!portal) return;
const label = swatch.dataset.label || '';
const locked = swatch.dataset.locked === 'true';
const date = swatch.dataset.unlockedDate || '';
const shoptalk = swatch.dataset.shoptalk || '';
const lockIcon = locked ? 'fa-lock' : 'fa-lock-open';
const lockText = locked ? 'Locked' : `Unlocked — ${date}`.trim();
portal.innerHTML = `
<h4 class="tt-title">${label}</h4>
${shoptalk ? `<p class="tt-shoptalk"><em>${shoptalk}</em></p>` : ''}
<p class="tt-lock"><i class="fa-solid ${lockIcon}"></i> ${lockText}</p>`;
const rect = swatch.getBoundingClientRect();
portal.style.display = 'block';
portal.style.position = 'fixed';
portal.style.top = `${rect.bottom + 8}px`;
portal.style.left = `${Math.min(rect.left, window.innerWidth - 280)}px`;
portal.style.zIndex = '9999';
}
function hideTooltip() {
if (!portal) return;
portal.style.display = 'none';
portal.innerHTML = '';
}
function dismiss() {
if (!activePreview) return;
clearTimeout(dismissTimer);
const paletteName = activePreview.dataset.palette;
activePreview.classList.remove('previewing');
activePreview.querySelector('.palette-ok').style.display = '';
document.body.classList.remove(paletteName);
if (originalPalette) document.body.classList.add(originalPalette);
activePreview = null;
originalPalette = null;
hideTooltip();
}
async function commitPalette(swatch, paletteName) {
// Silent commit — no animation, wipe already happened on preview
const old = originalPalette;
swatch.classList.remove('previewing');
swatch.querySelector('.palette-ok').style.display = '';
hideTooltip();
activePreview = null;
originalPalette = null;
clearTimeout(dismissTimer);
// Remove old palette, keep new one (already on body from preview)
if (old && old !== paletteName) {
document.body.classList.remove(old);
}
// Update active indicator
document.querySelectorAll('.swatch').forEach(sw => {
sw.classList.toggle('active', sw.classList.contains(paletteName));
});
// POST to server
const csrf = document.cookie.match(/csrftoken=([^;]+)/)?.[1] || '';
await fetch('/dashboard/set_palette', {
method: 'POST',
headers: { 'Accept': 'application/json', 'X-CSRFToken': csrf },
body: new URLSearchParams({ palette: paletteName }),
});
}
document.querySelectorAll('.palette-item .swatch').forEach(swatch => {
swatch.addEventListener('click', async (e) => {
e.stopPropagation();
if (swatch.classList.contains('previewing')) return;
dismiss(); // clear any existing preview
originalPalette = currentBodyPalette();
activePreview = swatch;
swatch.classList.add('previewing');
showTooltip(swatch);
swapPalette(swatch.dataset.palette);
swatch.querySelector('.palette-ok').style.display = 'flex';
// Auto-dismiss after 10s
dismissTimer = setTimeout(dismiss, 10000);
});
const okBtn = swatch.querySelector('.btn-confirm.palette-ok');
if (okBtn) {
okBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await commitPalette(swatch, swatch.dataset.palette);
});
}
});
document.addEventListener('click', () => dismiss());
};
const bindPaletteForms = () => {
document.querySelectorAll('form[action*="set_palette"]').forEach(form => {
form.addEventListener("submit", async (e) => {
@@ -29,12 +143,10 @@ const bindPaletteForms = () => {
});
if (!resp.ok) return;
const { palette } = await resp.json();
// Swap body palette class
[...document.body.classList]
.filter(c => c.startsWith("palette-"))
.forEach(c => document.body.classList.remove(c));
document.body.classList.add(palette);
// Update active swatch indicator
document.querySelectorAll(".swatch").forEach(sw => {
sw.classList.toggle("active", sw.classList.contains(palette));
});