PALETTE: swatch preview + tooltip + OK commit — TDD
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:
@@ -1,12 +1,7 @@
|
|||||||
// console.log("apps/scripts/dashboard.js loading");
|
|
||||||
const initialize = (inputSelector) => {
|
const initialize = (inputSelector) => {
|
||||||
// console.log("initialize called!");
|
|
||||||
const textInput = document.querySelector(inputSelector);
|
const textInput = document.querySelector(inputSelector);
|
||||||
if (!textInput) return;
|
if (!textInput) return;
|
||||||
textInput.oninput = () => {
|
textInput.oninput = () => textInput.classList.remove("is-invalid");
|
||||||
// console.log("oninput triggered");
|
|
||||||
textInput.classList.remove("is-invalid");
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindPaletteWheel = () => {
|
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 = () => {
|
const bindPaletteForms = () => {
|
||||||
document.querySelectorAll('form[action*="set_palette"]').forEach(form => {
|
document.querySelectorAll('form[action*="set_palette"]').forEach(form => {
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
@@ -29,12 +143,10 @@ const bindPaletteForms = () => {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
const { palette } = await resp.json();
|
const { palette } = await resp.json();
|
||||||
// Swap body palette class
|
|
||||||
[...document.body.classList]
|
[...document.body.classList]
|
||||||
.filter(c => c.startsWith("palette-"))
|
.filter(c => c.startsWith("palette-"))
|
||||||
.forEach(c => document.body.classList.remove(c));
|
.forEach(c => document.body.classList.remove(c));
|
||||||
document.body.classList.add(palette);
|
document.body.classList.add(palette);
|
||||||
// Update active swatch indicator
|
|
||||||
document.querySelectorAll(".swatch").forEach(sw => {
|
document.querySelectorAll(".swatch").forEach(sw => {
|
||||||
sw.classList.toggle("active", sw.classList.contains(palette));
|
sw.classList.toggle("active", sw.classList.contains(palette));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
from apps.applets.models import Applet
|
from apps.applets.models import Applet
|
||||||
@@ -6,31 +7,53 @@ from apps.lyric.models import User
|
|||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
|
|
||||||
|
|
||||||
class PaletteSwapTest(FunctionalTest):
|
def _setup_palette_applet():
|
||||||
def test_selecting_palette_updates_body_class_without_page_reload(self):
|
Applet.objects.get_or_create(
|
||||||
Applet.objects.get_or_create(slug="palette", defaults={
|
slug="palette",
|
||||||
"name": "Palette", "context": "dashboard",
|
defaults={"name": "Palette", "context": "dashboard"},
|
||||||
})
|
)
|
||||||
user, _ = User.objects.get_or_create(email="swap@test.io")
|
|
||||||
self.create_pre_authenticated_session("swap@test.io")
|
|
||||||
|
class PalettePreviewTest(FunctionalTest):
|
||||||
|
"""Clicking a swatch previews the palette on the whole body."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
_setup_palette_applet()
|
||||||
|
User.objects.get_or_create(email="preview@test.io")
|
||||||
|
self.create_pre_authenticated_session("preview@test.io")
|
||||||
self.browser.get(self.live_server_url)
|
self.browser.get(self.live_server_url)
|
||||||
|
|
||||||
body = self.browser.find_element(By.TAG_NAME, "body")
|
def _click_non_active_swatch(self):
|
||||||
self.assertIn("palette-default", body.get_attribute("class"))
|
swatch = self.wait_for(
|
||||||
|
|
||||||
# Mark the window — this survives JS execution but is wiped on a real reload
|
|
||||||
self.browser.execute_script("window._no_reload_marker = true;")
|
|
||||||
|
|
||||||
# Click OK on a non-active palette
|
|
||||||
btn = self.wait_for(
|
|
||||||
lambda: self.browser.find_element(
|
lambda: self.browser.find_element(
|
||||||
By.CSS_SELECTOR,
|
By.CSS_SELECTOR,
|
||||||
".palette-item:has(.swatch:not(.active)) .btn-confirm",
|
".palette-item:not(:has(.swatch.active)) .swatch",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
btn.click()
|
swatch.click()
|
||||||
|
return swatch
|
||||||
|
|
||||||
# Body palette class swaps without reload
|
def test_clicking_swatch_adds_preview_class_to_body(self):
|
||||||
|
swatch = self._click_non_active_swatch()
|
||||||
|
palette_name = swatch.get_attribute("data-palette")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_body_has_only_preview_palette_during_preview(self):
|
||||||
|
# Original palette class is swapped out — only the preview class is on body
|
||||||
|
swatch = self._click_non_active_swatch()
|
||||||
|
palette_name = swatch.get_attribute("data-palette")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertNotIn(
|
lambda: self.assertNotIn(
|
||||||
"palette-default",
|
"palette-default",
|
||||||
@@ -38,9 +61,257 @@ class PaletteSwapTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Marker still present — no full page reload occurred
|
def test_clicking_elsewhere_reverts_body_to_original_palette(self):
|
||||||
self.assertTrue(
|
swatch = self._click_non_active_swatch()
|
||||||
self.browser.execute_script("return window._no_reload_marker === true;")
|
palette_name = swatch.get_attribute("data-palette")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Click outside the applet
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertNotIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Original palette restored
|
||||||
|
self.assertIn(
|
||||||
|
"palette-default",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_swatch_gets_previewing_class_on_click(self):
|
||||||
|
self._click_non_active_swatch()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.previewing")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_previewing_class_removed_on_dismiss(self):
|
||||||
|
self._click_non_active_swatch()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.previewing")
|
||||||
|
)
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertEqual(
|
||||||
|
len(self.browser.find_elements(By.CSS_SELECTOR, ".swatch.previewing")),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_auto_dismiss_after_ten_seconds(self):
|
||||||
|
swatch = self._click_non_active_swatch()
|
||||||
|
palette_name = swatch.get_attribute("data-palette")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# After 10s the preview should clear automatically
|
||||||
|
self.wait_for_slow(
|
||||||
|
lambda: self.assertNotIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
),
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PaletteOkButtonTest(FunctionalTest):
|
||||||
|
"""OK btn (unlocked) / × btn (locked) appear centered on clicked swatch."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
_setup_palette_applet()
|
||||||
|
User.objects.get_or_create(email="okbtn@test.io")
|
||||||
|
self.create_pre_authenticated_session("okbtn@test.io")
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
|
||||||
|
def test_ok_btn_absent_before_click(self):
|
||||||
|
btns = self.browser.find_elements(By.CSS_SELECTOR, ".swatch .btn-confirm")
|
||||||
|
self.assertEqual(len([b for b in btns if b.is_displayed()]), 0)
|
||||||
|
|
||||||
|
def test_clicking_unlocked_swatch_shows_ok_btn_inside_swatch(self):
|
||||||
|
swatch = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
swatch.click()
|
||||||
|
ok = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(ok.is_displayed())
|
||||||
|
|
||||||
|
def test_clicking_locked_swatch_shows_disabled_times_btn(self):
|
||||||
|
locked = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".swatch.locked")
|
||||||
|
)
|
||||||
|
locked.click()
|
||||||
|
disabled = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".swatch.previewing .btn-disabled"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("×", disabled.text)
|
||||||
|
|
||||||
|
def test_clicking_ok_commits_palette_and_no_reload(self):
|
||||||
|
self.browser.execute_script("window._no_reload = true")
|
||||||
|
swatch = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
palette_name = swatch.get_attribute("data-palette")
|
||||||
|
swatch.click()
|
||||||
|
|
||||||
|
ok = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ok.click()
|
||||||
|
|
||||||
|
# Body ends up with only the new palette (preview cleared, committed)
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertNotIn(
|
||||||
|
"palette-default",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
palette_name,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(self.browser.execute_script("return window._no_reload === true"))
|
||||||
|
|
||||||
|
def test_btn_disappears_on_dismiss(self):
|
||||||
|
swatch = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
".palette-item:not(:has(.swatch.active)):not(:has(.swatch.locked)) .swatch",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
swatch.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".swatch.previewing .btn-confirm"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertEqual(
|
||||||
|
len([b for b in self.browser.find_elements(
|
||||||
|
By.CSS_SELECTOR, ".swatch .btn-confirm"
|
||||||
|
) if b.is_displayed()]),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PaletteTooltipTest(FunctionalTest):
|
||||||
|
"""Clicking a swatch shows a tooltip in #id_tooltip_portal."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
_setup_palette_applet()
|
||||||
|
self.user, _ = User.objects.get_or_create(email="palettett@test.io")
|
||||||
|
self.create_pre_authenticated_session("palettett@test.io")
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
|
||||||
|
def _click_swatch(self, selector=".swatch"):
|
||||||
|
swatch = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, selector)
|
||||||
|
)
|
||||||
|
swatch.click()
|
||||||
|
return swatch
|
||||||
|
|
||||||
|
def test_clicking_swatch_shows_tooltip_portal(self):
|
||||||
|
self._click_swatch()
|
||||||
|
portal = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_tooltip_portal")
|
||||||
|
)
|
||||||
|
self.assertTrue(portal.is_displayed())
|
||||||
|
|
||||||
|
def test_tooltip_title_is_colored_ter_user(self):
|
||||||
|
self._click_swatch()
|
||||||
|
title = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal .tt-title"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ter_user = self.browser.execute_script(
|
||||||
|
"return getComputedStyle(document.body).getPropertyValue('--terUser').trim()"
|
||||||
|
)
|
||||||
|
r, g, b = [int(x.strip()) for x in ter_user.split(",")]
|
||||||
|
color = self.browser.execute_script(
|
||||||
|
"return getComputedStyle(arguments[0]).color", title
|
||||||
|
)
|
||||||
|
self.assertIn(f"rgb({r}, {g}, {b})", color)
|
||||||
|
|
||||||
|
def test_tooltip_shows_shoptalk(self):
|
||||||
|
self._click_swatch()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal .tt-shoptalk"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unlocked_swatch_shows_lock_open_and_unlocked(self):
|
||||||
|
self._click_swatch(
|
||||||
|
".palette-item:not(:has(.swatch.locked)) .swatch"
|
||||||
|
)
|
||||||
|
lock_line = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("Unlocked", lock_line.text)
|
||||||
|
self.assertTrue(lock_line.find_elements(By.CSS_SELECTOR, ".fa-lock-open"))
|
||||||
|
|
||||||
|
def test_locked_swatch_shows_lock_and_locked(self):
|
||||||
|
self._click_swatch(".swatch.locked")
|
||||||
|
lock_line = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("Locked", lock_line.text)
|
||||||
|
self.assertTrue(lock_line.find_elements(By.CSS_SELECTOR, ".fa-lock"))
|
||||||
|
|
||||||
|
def test_unlocked_tooltip_shows_default_label(self):
|
||||||
|
self._click_swatch(".palette-item:not(:has(.swatch.locked)) .swatch")
|
||||||
|
lock_line = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_tooltip_portal .tt-lock"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("Default", lock_line.text)
|
||||||
|
|
||||||
|
def test_tooltip_dismisses_on_click_outside(self):
|
||||||
|
self._click_swatch()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertTrue(
|
||||||
|
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.browser.find_element(By.CSS_SELECTOR, "h1, h2").click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertFalse(
|
||||||
|
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ body.page-dashboard {
|
|||||||
|
|
||||||
.palette-scroll {
|
.palette-scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 3.5rem;
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 2rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item {
|
.palette-item {
|
||||||
@@ -16,15 +15,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: 100%;
|
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
flex: 1;
|
position: relative;
|
||||||
min-height: 0;
|
width: 7rem;
|
||||||
|
height: 7rem;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(var(--secUser), 1) 0%,
|
rgba(var(--secUser), 1) 0%,
|
||||||
@@ -47,4 +47,56 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
filter: saturate(0.4);
|
filter: saturate(0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.previewing {
|
||||||
|
border: 0.2rem solid rgba(var(--ninUser), 1);
|
||||||
|
box-shadow: 0 0 0.75rem rgba(var(--ninUser), 0.6);
|
||||||
|
|
||||||
|
&.locked {
|
||||||
|
opacity: 1;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK / × centred inside swatch — hidden until previewing
|
||||||
|
.palette-ok {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 2;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Palette tooltip portal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#id_tooltip_portal {
|
||||||
|
// Override .tt { display: none } — portal content is shown/hidden by JS
|
||||||
|
.tt-title,
|
||||||
|
.tt-shoptalk,
|
||||||
|
.tt-lock {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-title {
|
||||||
|
color: rgba(var(--terUser), 1);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-lock {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
i { margin-right: 0.25rem; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,16 +7,21 @@
|
|||||||
<div class="palette-scroll">
|
<div class="palette-scroll">
|
||||||
{% for palette in palettes %}
|
{% for palette in palettes %}
|
||||||
<div class="palette-item">
|
<div class="palette-item">
|
||||||
<div class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"></div>
|
<div
|
||||||
|
class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"
|
||||||
|
data-palette="{{ palette.name }}"
|
||||||
|
data-label="{{ palette.label }}"
|
||||||
|
data-locked="{{ palette.locked|yesno:'true,false' }}"
|
||||||
|
data-unlocked-date="{% if not palette.locked %}Default{% endif %}"
|
||||||
|
data-shoptalk="Placeholder"
|
||||||
|
>
|
||||||
{% if not palette.locked %}
|
{% if not palette.locked %}
|
||||||
<form method="POST" action="{% url "set_palette" %}">
|
<button type="button" class="btn btn-confirm palette-ok" hidden>OK</button>
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" name="palette" value="{{ palette.name }}" class="btn btn-confirm">OK</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="btn btn-disabled">×</span>
|
<button type="button" class="btn btn-disabled palette-ok" hidden>×</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
initialize("#id_text");
|
initialize("#id_text");
|
||||||
bindPaletteForms();
|
bindPaletteSwatches();
|
||||||
bindPaletteWheel();
|
bindPaletteWheel();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -20,5 +20,6 @@
|
|||||||
{% include "apps/dashboard/_partials/_applets.html" %}
|
{% include "apps/dashboard/_partials/_applets.html" %}
|
||||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_dash_applet_menu" %}
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_dash_applet_menu" %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="id_tooltip_portal" class="token-tooltip" style="display:none;"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
Reference in New Issue
Block a user