palette tooltip: contextual description per lock state; earned date below Unlocked in green — TDD
- _palettes_for_user: shoptalk→description; "available by default" / "explore to unlock" / "recognized via {Title}"; unlocked_date key carries earned_at ISO for Note-unlocked
- template: data-shoptalk→data-description; data-unlocked-date now holds ISO datetime (was literal "Default")
- dashboard.js: lockText drops "— Default"; dateLine renders "Apr 22, 2026 · 3:05 AM" after Unlocked line
- _palette-picker.scss: tt-shoptalk→tt-description in display:block list; tt-date added
- _tooltips.scss: .tt-date mirrors .tt-expiry w. --priGn colour
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,17 +33,26 @@ const bindPaletteSwatches = () => {
|
||||
|
||||
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();
|
||||
const label = swatch.dataset.label || '';
|
||||
const locked = swatch.dataset.locked === 'true';
|
||||
const description = swatch.dataset.description || '';
|
||||
const unlockedDate = swatch.dataset.unlockedDate || '';
|
||||
const lockIcon = locked ? 'fa-lock' : 'fa-lock-open';
|
||||
const lockText = locked ? 'Locked' : 'Unlocked';
|
||||
|
||||
let dateLine = '';
|
||||
if (unlockedDate) {
|
||||
const dt = new Date(unlockedDate);
|
||||
const dateStr = dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
const timeStr = dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
dateLine = `<p class="tt-date">${dateStr} · ${timeStr}</p>`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
${description ? `<p class="tt-description"><em>${description}</em></p>` : ''}
|
||||
<p class="tt-lock"><i class="fa-solid ${lockIcon}"></i> ${lockText}</p>
|
||||
${dateLine}`;
|
||||
|
||||
const rect = swatch.getBoundingClientRect();
|
||||
portal.style.display = 'block';
|
||||
|
||||
@@ -358,7 +358,7 @@ class NotePaletteContextTest(TestCase):
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertFalse(bardo["locked"])
|
||||
|
||||
def test_note_palette_shoptalk_contains_note_title(self):
|
||||
def test_note_palette_description_contains_note_title(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
@@ -366,7 +366,41 @@ class NotePaletteContextTest(TestCase):
|
||||
response = self.client.get("/")
|
||||
palettes = response.context["palettes"]
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertIn("Stargazer", bardo["shoptalk"])
|
||||
self.assertIn("Stargazer", bardo["description"])
|
||||
|
||||
def test_default_palette_description_is_available_by_default(self):
|
||||
response = self.client.get("/")
|
||||
palettes = response.context["palettes"]
|
||||
default = next(p for p in palettes if p["name"] == "palette-default")
|
||||
self.assertEqual(default["description"], "available by default")
|
||||
|
||||
def test_locked_palette_description_is_explore_to_unlock(self):
|
||||
response = self.client.get("/")
|
||||
palettes = response.context["palettes"]
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertEqual(bardo["description"], "explore to unlock")
|
||||
|
||||
def test_note_palette_description_is_recognized_via(self):
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now(),
|
||||
palette="palette-bardo",
|
||||
)
|
||||
response = self.client.get("/")
|
||||
palettes = response.context["palettes"]
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertEqual(bardo["description"], "recognized via Stargazer")
|
||||
|
||||
def test_note_palette_entry_includes_unlocked_date_iso(self):
|
||||
earned = timezone.now()
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=earned,
|
||||
palette="palette-bardo",
|
||||
)
|
||||
response = self.client.get("/")
|
||||
palettes = response.context["palettes"]
|
||||
bardo = next(p for p in palettes if p["name"] == "palette-bardo")
|
||||
self.assertIn("unlocked_date", bardo)
|
||||
self.assertEqual(bardo["unlocked_date"], earned.isoformat())
|
||||
|
||||
def test_note_without_palette_field_keeps_swatch_locked(self):
|
||||
Note.objects.create(
|
||||
|
||||
@@ -51,7 +51,10 @@ PALETTES = _PALETTE_DEFS
|
||||
|
||||
def _palettes_for_user(user):
|
||||
if not (user and user.is_authenticated):
|
||||
return [dict(p, shoptalk="Placeholder") for p in _PALETTE_DEFS]
|
||||
return [
|
||||
dict(p, description="available by default" if not p["locked"] else "explore to unlock")
|
||||
for p in _PALETTE_DEFS
|
||||
]
|
||||
granted = {
|
||||
r.palette: r
|
||||
for r in Note.objects.filter(user=user, palette__isnull=False).exclude(palette="")
|
||||
@@ -63,9 +66,12 @@ def _palettes_for_user(user):
|
||||
if r and p["locked"]:
|
||||
entry["locked"] = False
|
||||
title = _NOTE_TITLES.get(r.slug, r.slug.capitalize())
|
||||
entry["shoptalk"] = f"{title} · {r.earned_at.strftime('%b %d, %Y').replace(' 0', ' ')}"
|
||||
entry["description"] = f"recognized via {title}"
|
||||
entry["unlocked_date"] = r.earned_at.isoformat()
|
||||
elif not p["locked"]:
|
||||
entry["description"] = "available by default"
|
||||
else:
|
||||
entry["shoptalk"] = "Placeholder"
|
||||
entry["description"] = "explore to unlock"
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@
|
||||
#id_tooltip_portal {
|
||||
// Override .tt { display: none } — portal content is shown/hidden by JS
|
||||
.tt-title,
|
||||
.tt-shoptalk,
|
||||
.tt-description,
|
||||
.tt-date,
|
||||
.tt-lock {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.tt-description { padding: 0.125rem; font-size: 0.75rem; }
|
||||
.tt-shoptalk { font-size: 0.75rem; opacity: 0.75; }
|
||||
.tt-expiry { font-size: 1rem; color: rgba(var(--priRd), 1); }
|
||||
.tt-date { font-size: 1rem; color: rgba(var(--priGn), 1); }
|
||||
}
|
||||
|
||||
.token-tooltip,
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
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="{{ palette.shoptalk }}"
|
||||
data-unlocked-date="{{ palette.unlocked_date|default:'' }}"
|
||||
data-description="{{ palette.description }}"
|
||||
>
|
||||
{% if not palette.locked %}
|
||||
<button type="button" class="btn btn-confirm palette-ok" hidden>OK</button>
|
||||
|
||||
Reference in New Issue
Block a user