note palette modal: NVM closes confirm only; confirm visibility via style.display; swatch labels from _PALETTE_DEFS; recognitions block — TDD
- note-page.js: NVM hides confirm (style.display='none') — swatch modal stays open;
_showConfirm/_hideConfirm use style.display to bypass CSS specificity issues;
_doSetPalette reads data-palette-label before modal closes; appends
.note-recognitions__palette-line w. dim+bold markup after OK
- billboard/views.py: import _PALETTE_DEFS; _PALETTE_LABELS dict; _palette_opts()
enriches palette_options w. {name, label}; my_notes adds palette_label to note_items
- _note.scss: confirmed palette swatch uses gradient (palette vars cascade from
palette-* class); hardcoded bardo/sheol bg overrides removed; .note-recognitions block
w. .note-recognitions__header (tt-sign-section-header style) & __dim (tt-dim style);
.note-swatch-label in terUser bold; .note-item__palette gradient; confirm display:none default
- my_notes.html: p.name/p.label replaces slice hack; data-palette-label on swatch rows;
Recognitions block w. dim spans & strong values; removes hidden attr from confirm
- IT: test_palette_modal_renders_swatch_labels; test_also_saves_user_palette
- FT: NVM test corrected — modal stays open, confirm is_displayed() False; T2a URL fix
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:
@@ -37,6 +37,16 @@
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
function _showConfirm(modal) {
|
||||
var el = modal && modal.querySelector('.note-palette-confirm');
|
||||
if (el) el.style.display = 'flex';
|
||||
}
|
||||
|
||||
function _hideConfirm(modal) {
|
||||
var el = modal && modal.querySelector('.note-palette-confirm');
|
||||
if (el) el.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── modal lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
function _openModal() {
|
||||
@@ -49,8 +59,7 @@
|
||||
_wireModal();
|
||||
}
|
||||
_activeItem.classList.add('note-item--active');
|
||||
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
_hideConfirm(_activeModal());
|
||||
}
|
||||
|
||||
function _closeModal() {
|
||||
@@ -68,14 +77,12 @@
|
||||
clearTimeout(_dismissTimer);
|
||||
_dismissTimer = null;
|
||||
_revertBodyPalette();
|
||||
// Remove .previewing from any swatch in the active modal
|
||||
var modal = _activeModal();
|
||||
if (modal) {
|
||||
modal.querySelectorAll('.note-swatch-body.previewing').forEach(function (s) {
|
||||
s.classList.remove('previewing');
|
||||
});
|
||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
_hideConfirm(modal);
|
||||
}
|
||||
_selectedPalette = null;
|
||||
_originalPalette = null;
|
||||
@@ -90,7 +97,6 @@
|
||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||
body.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
// Clear any in-progress preview first
|
||||
if (_selectedPalette) _revertPreview();
|
||||
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
@@ -98,11 +104,8 @@
|
||||
|
||||
body.classList.add('previewing');
|
||||
_swapBodyPalette(_selectedPalette);
|
||||
_showConfirm(modal);
|
||||
|
||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = false;
|
||||
|
||||
// Auto-revert after 10s
|
||||
_dismissTimer = setTimeout(function () {
|
||||
_revertPreview();
|
||||
}, 10000);
|
||||
@@ -117,7 +120,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm NVM → revert preview, hide confirm
|
||||
// Confirm NVM → revert preview only; main swatch modal stays open
|
||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -135,7 +138,11 @@
|
||||
var url = _activeItem.dataset.setPaletteUrl;
|
||||
var palette = _selectedPalette;
|
||||
var item = _activeItem;
|
||||
// Body already shows new palette from preview — keep it by not reverting.
|
||||
// Read label from swatch row while modal is still in DOM
|
||||
var swatchRow = _activeModal() && _activeModal().querySelector('.' + palette + '[data-palette-label]');
|
||||
var paletteLabel = swatchRow
|
||||
? swatchRow.dataset.paletteLabel
|
||||
: palette.slice(8).replace(/^\w/, function (c) { return c.toUpperCase(); });
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
@@ -154,13 +161,19 @@
|
||||
swatch.className = 'note-item__palette ' + palette;
|
||||
imageBox.parentNode.replaceChild(swatch, imageBox);
|
||||
}
|
||||
var list = item.querySelector('.note-recognitions__list');
|
||||
if (list && !item.querySelector('.note-recognitions__palette-line')) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'note-recognitions__palette-line';
|
||||
li.innerHTML = '<span class="note-recognitions__dim">Palette:</span> <strong>' + paletteLabel + '</strong>';
|
||||
list.appendChild(li);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function _init() {
|
||||
// Image-box click → open modal
|
||||
document.querySelectorAll('.note-item__image-box').forEach(function (box) {
|
||||
box.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
@@ -169,7 +182,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Body click → dismiss modal (and revert any preview)
|
||||
// Body click → dismiss modal and revert any preview
|
||||
document.body.addEventListener('click', function () {
|
||||
if (_selectedPalette) _revertPreview();
|
||||
_closeModal();
|
||||
|
||||
@@ -214,6 +214,17 @@ class NotePageViewTest(TestCase):
|
||||
self.assertContains(response, 'class="note-item__description"')
|
||||
self.assertContains(response, 'class="note-item__image-box"')
|
||||
|
||||
def test_palette_modal_renders_swatch_labels(self):
|
||||
"""Each palette option in the swatch modal should display its human-readable
|
||||
label next to the swatch body so the user knows what they are choosing."""
|
||||
Note.objects.create(
|
||||
user=self.user, slug="stargazer", earned_at=timezone.now()
|
||||
)
|
||||
response = self.client.get("/billboard/my-notes/")
|
||||
self.assertContains(response, 'class="note-swatch-label"')
|
||||
self.assertContains(response, "Bardo")
|
||||
self.assertContains(response, "Sheol")
|
||||
|
||||
|
||||
class NoteSetPaletteViewTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -8,10 +8,13 @@ from django.shortcuts import redirect, render
|
||||
from apps.applets.utils import applet_context, apply_applet_toggle
|
||||
from apps.dashboard.forms import LineForm
|
||||
from apps.dashboard.models import Post
|
||||
from apps.dashboard.views import _PALETTE_DEFS
|
||||
from apps.drama.models import GameEvent, Note, ScrollPosition
|
||||
from apps.epic.models import Room
|
||||
from apps.epic.utils import rooms_for_user
|
||||
|
||||
_PALETTE_LABELS = {p["name"]: p["label"] for p in _PALETTE_DEFS}
|
||||
|
||||
|
||||
def _recent_posts(user, limit=3):
|
||||
return (
|
||||
@@ -88,11 +91,15 @@ def room_scroll(request, room_id):
|
||||
})
|
||||
|
||||
|
||||
def _palette_opts(names):
|
||||
return [{"name": n, "label": _PALETTE_LABELS.get(n, n)} for n in names]
|
||||
|
||||
|
||||
_NOTE_META = {
|
||||
"stargazer": {
|
||||
"title": "Stargazer",
|
||||
"description": "You saved your first personal sky chart.",
|
||||
"palette_options": ["palette-bardo", "palette-sheol"],
|
||||
"palette_options": _palette_opts(["palette-bardo", "palette-sheol"]),
|
||||
},
|
||||
"schizo": {
|
||||
"title": "Schizo",
|
||||
@@ -136,6 +143,7 @@ def my_notes(request):
|
||||
"title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
|
||||
"description": _NOTE_META.get(n.slug, {}).get("description", ""),
|
||||
"palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
|
||||
"palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
|
||||
}
|
||||
for n in qs
|
||||
]
|
||||
|
||||
@@ -272,6 +272,12 @@ class StargazerNoteFromDashboardTest(FunctionalTest):
|
||||
original_palette,
|
||||
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||
)
|
||||
# Swatch modal remains open after NVM; only the confirm bar is hidden
|
||||
self.assertTrue(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
||||
)
|
||||
confirm_el = self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm")
|
||||
self.assertFalse(confirm_el.is_displayed())
|
||||
|
||||
# ── T2c ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -101,6 +101,36 @@
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.note-recognitions {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.note-recognitions__header {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.55;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
.note-recognitions__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
|
||||
strong { font-weight: 700; }
|
||||
}
|
||||
|
||||
.note-recognitions__dim {
|
||||
opacity: 0.6;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Image box — right-side; must have a defined size so Selenium can interact with it.
|
||||
@@ -112,22 +142,35 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(var(--priUser), 0.12);
|
||||
border: 1px dashed rgba(var(--priUser), 0.7);
|
||||
border: 1px dashed rgba(var(--terUser), 0.75);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
color: rgba(var(--terUser), 0.75);
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
|
||||
// Unlocked palette swatch (right side, same footprint as image-box)
|
||||
// Confirmed palette swatch — right-side thumbnail, same gradient as .note-swatch-body.
|
||||
// palette-* class is on the element so CSS vars cascade from that palette automatically.
|
||||
.note-item__palette {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 2px solid rgba(var(--priUser), 0.4);
|
||||
border-radius: 0.25rem;
|
||||
border: 0.15rem solid rgba(var(--secUser), 0.5);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--secUser), 1) 0%,
|
||||
rgba(var(--secUser), 1) 30%,
|
||||
rgba(var(--priUser), 1) 30%,
|
||||
rgba(var(--priUser), 1) 70%,
|
||||
rgba(var(--terUser), 1) 70%,
|
||||
rgba(var(--terUser), 1) 85%,
|
||||
rgba(var(--quaUser), 1) 85%,
|
||||
rgba(var(--quaUser), 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
// ── Palette modal ──────────────────────────────────────────────────────────
|
||||
@@ -143,7 +186,7 @@
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
min-width: 10rem;
|
||||
min-width: 14rem;
|
||||
|
||||
&:not([hidden]) { display: flex; flex-direction: column; }
|
||||
|
||||
@@ -151,11 +194,11 @@
|
||||
> [class*="palette-"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
.note-swatch-body {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
// Gradient uses vars scoped to the parent palette-* class,
|
||||
@@ -185,24 +228,26 @@
|
||||
box-shadow: 0 0 0.75rem rgba(var(--ninUser), 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.note-swatch-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: rgba(var(--terUser), 1);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Palette swatch color fills ─────────────────────────────────────────────
|
||||
|
||||
.palette-bardo .note-swatch-body,
|
||||
.note-item__palette.palette-bardo {
|
||||
background: #2a1a2e;
|
||||
}
|
||||
|
||||
.palette-sheol .note-swatch-body,
|
||||
.note-item__palette.palette-sheol {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
// ── Confirm submenu — floats below modal, out of its flow ─────────────────
|
||||
|
||||
.note-palette-confirm {
|
||||
// display controlled entirely by JS (style.display) — hidden until a swatch is previewed
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
@@ -213,9 +258,6 @@
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
gap: 0.4rem;
|
||||
|
||||
&:not([hidden]) { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; }
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
<div class="note-item__body">
|
||||
<p class="note-item__title">{{ item.title }}</p>
|
||||
<p class="note-item__description">{{ item.description }}</p>
|
||||
<div class="note-recognitions">
|
||||
<div class="note-recognitions__header">Recognitions</div>
|
||||
<ul class="note-recognitions__list">
|
||||
<li><span class="note-recognitions__dim">Title:</span> <strong>{{ item.title }}</strong></li>
|
||||
{% if item.obj.palette %}
|
||||
<li class="note-recognitions__palette-line"><span class="note-recognitions__dim">Palette:</span> <strong>{{ item.palette_label }}</strong></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if item.obj.palette %}
|
||||
@@ -26,12 +35,13 @@
|
||||
{% if not item.obj.palette and item.palette_options %}
|
||||
<template class="note-palette-modal-tpl">
|
||||
<div class="note-palette-modal">
|
||||
{% for palette_name in item.palette_options %}
|
||||
<div class="{{ palette_name }}">
|
||||
{% for p in item.palette_options %}
|
||||
<div class="{{ p.name }}" data-palette-label="{{ p.label }}">
|
||||
<div class="note-swatch-body"></div>
|
||||
<span class="note-swatch-label">{{ p.label }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="note-palette-confirm" hidden>
|
||||
<div class="note-palette-confirm">
|
||||
<p>Lock in this palette?</p>
|
||||
<button type="button" class="btn btn-confirm">OK</button>
|
||||
<button type="button" class="btn btn-cancel">NVM</button>
|
||||
|
||||
Reference in New Issue
Block a user