note palette: swatch previews body palette, NVM reverts, OK saves sitewide; note_set_palette also saves user.palette — TDD
- note-page.js: body class swap on swatch click; 10s auto-revert timer; NVM reverts; .note-item--active persists border/glow while modal open; .previewing on swatch - billboard/views.py: note_set_palette also saves user.palette via _unlocked_palettes_for_user - _note.scss: .note-swatch-body gradient (palette vars cascade from parent palette-* class); .previewing state; .note-item--active; note-palette-modal tooltip glass; note-palette-confirm floats below modal (position:absolute, out of flow) - my_notes.html: note-item__body wrapper; image-box right; swatch row OK buttons removed - FTs: T2a URL fix (/recognition → /my-notes); T2b split into preview+persist & NVM tests; NoteSetPaletteViewTest.test_also_saves_user_palette IT 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:
@@ -1,9 +1,10 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var _state = 'closed'; // 'closed' | 'open' | 'previewing'
|
|
||||||
var _selectedPalette = null;
|
var _selectedPalette = null;
|
||||||
var _activeItem = null;
|
var _activeItem = null;
|
||||||
|
var _originalPalette = null;
|
||||||
|
var _dismissTimer = null;
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -15,6 +16,22 @@
|
|||||||
return Array.from(el.classList).find(function (c) { return c.startsWith('palette-'); }) || '';
|
return Array.from(el.classList).find(function (c) { return c.startsWith('palette-'); }) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _currentBodyPalette() {
|
||||||
|
return Array.from(document.body.classList).find(function (c) { return c.startsWith('palette-'); }) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _swapBodyPalette(paletteName) {
|
||||||
|
var old = _currentBodyPalette();
|
||||||
|
if (old) document.body.classList.remove(old);
|
||||||
|
document.body.classList.add(paletteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _revertBodyPalette() {
|
||||||
|
var current = _currentBodyPalette();
|
||||||
|
if (current) document.body.classList.remove(current);
|
||||||
|
if (_originalPalette) document.body.classList.add(_originalPalette);
|
||||||
|
}
|
||||||
|
|
||||||
function _getCsrf() {
|
function _getCsrf() {
|
||||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
@@ -23,7 +40,6 @@
|
|||||||
// ── modal lifecycle ───────────────────────────────────────────────────────
|
// ── modal lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _openModal() {
|
function _openModal() {
|
||||||
// Clone from <template> if the modal is not in the DOM yet.
|
|
||||||
var existing = _activeModal();
|
var existing = _activeModal();
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
var tpl = _activeItem.querySelector('.note-palette-modal-tpl');
|
var tpl = _activeItem.querySelector('.note-palette-modal-tpl');
|
||||||
@@ -32,15 +48,37 @@
|
|||||||
_activeItem.appendChild(clone);
|
_activeItem.appendChild(clone);
|
||||||
_wireModal();
|
_wireModal();
|
||||||
}
|
}
|
||||||
_state = 'open';
|
_activeItem.classList.add('note-item--active');
|
||||||
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
||||||
if (confirmEl) confirmEl.hidden = true;
|
if (confirmEl) confirmEl.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _closeModal() {
|
function _closeModal() {
|
||||||
_state = 'closed';
|
clearTimeout(_dismissTimer);
|
||||||
var modal = _activeModal();
|
_dismissTimer = null;
|
||||||
|
var modal = _activeModal();
|
||||||
if (modal) modal.remove();
|
if (modal) modal.remove();
|
||||||
|
if (_activeItem) _activeItem.classList.remove('note-item--active');
|
||||||
|
_activeItem = null;
|
||||||
|
_selectedPalette = null;
|
||||||
|
_originalPalette = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _revertPreview() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
_selectedPalette = null;
|
||||||
|
_originalPalette = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire event listeners onto the freshly-cloned modal DOM.
|
// Wire event listeners onto the freshly-cloned modal DOM.
|
||||||
@@ -48,28 +86,30 @@
|
|||||||
var modal = _activeModal();
|
var modal = _activeModal();
|
||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
|
|
||||||
// Swatch body → preview (remove modal from DOM)
|
// Swatch body click → preview palette sitewide + show confirm
|
||||||
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
|
||||||
body.addEventListener('click', function (e) {
|
body.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
_selectedPalette = _paletteClass(body.parentElement);
|
// Clear any in-progress preview first
|
||||||
_state = 'previewing';
|
if (_selectedPalette) _revertPreview();
|
||||||
modal.remove();
|
|
||||||
});
|
_selectedPalette = _paletteClass(body.parentElement);
|
||||||
});
|
_originalPalette = _currentBodyPalette();
|
||||||
|
|
||||||
|
body.classList.add('previewing');
|
||||||
|
_swapBodyPalette(_selectedPalette);
|
||||||
|
|
||||||
// OK button (not inside confirm submenu) → show confirm submenu
|
|
||||||
modal.querySelectorAll('.btn.btn-confirm').forEach(function (btn) {
|
|
||||||
if (btn.closest('.note-palette-confirm')) return;
|
|
||||||
btn.addEventListener('click', function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
_selectedPalette = _paletteClass(btn.parentElement);
|
|
||||||
var confirmEl = modal.querySelector('.note-palette-confirm');
|
var confirmEl = modal.querySelector('.note-palette-confirm');
|
||||||
if (confirmEl) confirmEl.hidden = false;
|
if (confirmEl) confirmEl.hidden = false;
|
||||||
|
|
||||||
|
// Auto-revert after 10s
|
||||||
|
_dismissTimer = setTimeout(function () {
|
||||||
|
_revertPreview();
|
||||||
|
}, 10000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm YES → POST and update DOM
|
// Confirm OK → commit palette sitewide
|
||||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function (e) {
|
btn.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -77,15 +117,15 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm NVM → hide confirm submenu
|
// Confirm NVM → revert preview, hide confirm
|
||||||
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
modal.querySelectorAll('.note-palette-confirm .btn.btn-cancel').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function (e) {
|
btn.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
btn.closest('.note-palette-confirm').hidden = true;
|
_revertPreview();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop all other modal clicks from reaching body handler.
|
// Stop modal clicks from reaching the body dismiss handler.
|
||||||
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
modal.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +134,8 @@
|
|||||||
function _doSetPalette() {
|
function _doSetPalette() {
|
||||||
var url = _activeItem.dataset.setPaletteUrl;
|
var url = _activeItem.dataset.setPaletteUrl;
|
||||||
var palette = _selectedPalette;
|
var palette = _selectedPalette;
|
||||||
|
var item = _activeItem;
|
||||||
|
// Body already shows new palette from preview — keep it by not reverting.
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -106,7 +148,7 @@
|
|||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function () {
|
.then(function () {
|
||||||
_closeModal();
|
_closeModal();
|
||||||
var imageBox = _activeItem.querySelector('.note-item__image-box');
|
var imageBox = item.querySelector('.note-item__image-box');
|
||||||
if (imageBox) {
|
if (imageBox) {
|
||||||
var swatch = document.createElement('div');
|
var swatch = document.createElement('div');
|
||||||
swatch.className = 'note-item__palette ' + palette;
|
swatch.className = 'note-item__palette ' + palette;
|
||||||
@@ -127,11 +169,10 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Body click → restore modal when previewing
|
// Body click → dismiss modal (and revert any preview)
|
||||||
document.body.addEventListener('click', function () {
|
document.body.addEventListener('click', function () {
|
||||||
if (_state === 'previewing') {
|
if (_selectedPalette) _revertPreview();
|
||||||
_openModal();
|
_closeModal();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,6 +259,17 @@ class NoteSetPaletteViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_also_saves_user_palette(self):
|
||||||
|
"""note_set_palette must persist the choice to user.palette so the
|
||||||
|
palette survives page navigation (sitewide commitment)."""
|
||||||
|
self.client.post(
|
||||||
|
self.url,
|
||||||
|
data=_json.dumps({"palette": "palette-bardo"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.assertEqual(self.user.palette, "palette-bardo")
|
||||||
|
|
||||||
|
|
||||||
class SaveScrollPositionTest(TestCase):
|
class SaveScrollPositionTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -110,14 +110,20 @@ _NOTE_META = {
|
|||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def note_set_palette(request, slug):
|
def note_set_palette(request, slug):
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from apps.dashboard.views import _unlocked_palettes_for_user
|
||||||
try:
|
try:
|
||||||
note = Note.objects.get(user=request.user, slug=slug)
|
note = Note.objects.get(user=request.user, slug=slug)
|
||||||
except Note.DoesNotExist:
|
except Note.DoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
body = json.loads(request.body)
|
body = json.loads(request.body)
|
||||||
note.palette = body.get("palette", "")
|
palette = body.get("palette", "")
|
||||||
|
note.palette = palette
|
||||||
note.save(update_fields=["palette"])
|
note.save(update_fields=["palette"])
|
||||||
|
# Commit as the user's active sitewide palette now that the Note unlocks it.
|
||||||
|
if palette in _unlocked_palettes_for_user(request.user):
|
||||||
|
request.user.palette = palette
|
||||||
|
request.user.save(update_fields=["palette"])
|
||||||
return JsonResponse({"ok": True})
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ class StargazerNoteFromDashboardTest(FunctionalTest):
|
|||||||
# FYI navigates to Note page
|
# FYI navigates to Note page
|
||||||
fyi.click()
|
fyi.click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertRegex(self.browser.current_url, r"/billboard/recognition")
|
lambda: self.assertRegex(self.browser.current_url, r"/billboard/my-notes")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Note page: one Stargazer item
|
# Note page: one Stargazer item
|
||||||
@@ -172,62 +172,106 @@ class StargazerNoteFromDashboardTest(FunctionalTest):
|
|||||||
|
|
||||||
# ── T2b ──────────────────────────────────────────────────────────────────
|
# ── T2b ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_note_page_palette_modal_flow(self):
|
def _open_modal_and_click_bardo(self):
|
||||||
"""Note page palette modal: image-box opens modal, swatch preview,
|
"""Helper: navigate to /billboard/my-notes/, open modal, click bardo swatch body.
|
||||||
body-click restores modal, OK raises confirm submenu, confirm sets palette."""
|
Returns (modal, confirm_menu) after the confirm bar is visible."""
|
||||||
Note.objects.create(
|
|
||||||
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
|
||||||
)
|
|
||||||
self.create_pre_authenticated_session("stargazer@test.io")
|
|
||||||
self.browser.get(self.live_server_url + "/billboard/my-notes/")
|
self.browser.get(self.live_server_url + "/billboard/my-notes/")
|
||||||
|
|
||||||
image_box = self.wait_for(
|
image_box = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item__image-box")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item__image-box")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clicking ? opens palette modal
|
|
||||||
image_box.click()
|
image_box.click()
|
||||||
modal = self.wait_for(
|
modal = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal")
|
||||||
)
|
)
|
||||||
modal.find_element(By.CSS_SELECTOR, ".palette-bardo")
|
|
||||||
modal.find_element(By.CSS_SELECTOR, ".palette-sheol")
|
|
||||||
|
|
||||||
# Clicking a swatch body previews the palette and dismisses the modal
|
|
||||||
bardo_body = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .note-swatch-body")
|
bardo_body = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .note-swatch-body")
|
||||||
self.browser.execute_script(
|
self.browser.execute_script(
|
||||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||||
bardo_body,
|
bardo_body,
|
||||||
)
|
)
|
||||||
self.wait_for(lambda: self.assertFalse(
|
|
||||||
self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
|
||||||
))
|
|
||||||
|
|
||||||
# Clicking elsewhere ends preview and restores the modal
|
|
||||||
self.browser.find_element(By.TAG_NAME, "body").click()
|
|
||||||
modal = self.wait_for(
|
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-modal")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clicking OK on the swatch raises a confirmation submenu
|
|
||||||
ok_btn = modal.find_element(By.CSS_SELECTOR, ".palette-bardo .btn.btn-confirm")
|
|
||||||
ok_btn.click()
|
|
||||||
confirm_menu = self.wait_for(
|
confirm_menu = self.wait_for(
|
||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-palette-confirm")
|
||||||
)
|
)
|
||||||
|
return modal, confirm_menu
|
||||||
|
|
||||||
# Confirming sets palette, closes modal, replaces image-box with palette swatch
|
def test_note_page_swatch_previews_palette_sitewide_and_ok_persists(self):
|
||||||
confirm_menu.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
|
"""Clicking a swatch previews that palette on the whole body.
|
||||||
|
OK commits it — Note.palette and user.palette both saved — so it
|
||||||
|
survives navigation to a new page."""
|
||||||
|
Note.objects.create(
|
||||||
|
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||||
|
)
|
||||||
|
self.create_pre_authenticated_session("stargazer@test.io")
|
||||||
|
|
||||||
|
modal, confirm = self._open_modal_and_click_bardo()
|
||||||
|
|
||||||
|
# Swatch click previews bardo on the whole body
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"palette-bardo",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Modal still open
|
||||||
|
self.assertTrue(self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal"))
|
||||||
|
|
||||||
|
# OK → modal closes, ? box replaced by bardo swatch
|
||||||
|
confirm.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
|
||||||
self.wait_for(lambda: self.assertFalse(
|
self.wait_for(lambda: self.assertFalse(
|
||||||
self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
self.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
||||||
))
|
))
|
||||||
item = self.browser.find_element(By.CSS_SELECTOR, ".note-item")
|
item = self.browser.find_element(By.CSS_SELECTOR, ".note-item")
|
||||||
self.assertTrue(
|
self.assertTrue(item.find_elements(By.CSS_SELECTOR, ".note-item__palette.palette-bardo"))
|
||||||
item.find_elements(By.CSS_SELECTOR, ".note-item__palette.palette-bardo")
|
self.assertFalse(item.find_elements(By.CSS_SELECTOR, ".note-item__image-box"))
|
||||||
|
|
||||||
|
# Navigate away — palette persists (user.palette was saved)
|
||||||
|
self.browser.get(self.live_server_url)
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"palette-bardo",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertFalse(
|
|
||||||
item.find_elements(By.CSS_SELECTOR, ".note-item__image-box")
|
def test_note_swatch_nvm_reverts_body_palette(self):
|
||||||
|
"""NVM in the confirm bar reverts the sitewide body palette back to
|
||||||
|
what it was before the swatch was clicked."""
|
||||||
|
Note.objects.create(
|
||||||
|
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||||
)
|
)
|
||||||
|
self.create_pre_authenticated_session("stargazer@test.io")
|
||||||
|
|
||||||
|
# Record the original palette before opening the modal
|
||||||
|
self.browser.get(self.live_server_url + "/billboard/my-notes/")
|
||||||
|
original_classes = self.browser.find_element(
|
||||||
|
By.TAG_NAME, "body"
|
||||||
|
).get_attribute("class")
|
||||||
|
original_palette = next(
|
||||||
|
(c for c in original_classes.split() if c.startswith("palette-")), None
|
||||||
|
)
|
||||||
|
|
||||||
|
modal, confirm = self._open_modal_and_click_bardo()
|
||||||
|
|
||||||
|
# Bardo is previewed
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertIn(
|
||||||
|
"palette-bardo",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# NVM reverts
|
||||||
|
confirm.find_element(By.CSS_SELECTOR, ".btn.btn-cancel").click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertNotIn(
|
||||||
|
"palette-bardo",
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if original_palette:
|
||||||
|
self.assertIn(
|
||||||
|
original_palette,
|
||||||
|
self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"),
|
||||||
|
)
|
||||||
|
|
||||||
# ── T2c ──────────────────────────────────────────────────────────────────
|
# ── T2c ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ describe('Note.showBanner', () => {
|
|||||||
|
|
||||||
// ── T7 ── NVM button ──────────────────────────────────────────────────────
|
// ── T7 ── NVM button ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
it('T7: banner has a .btn.btn-danger NVM button', () => {
|
it('T7: banner has a .btn.btn-cancel NVM button', () => {
|
||||||
Note.showBanner(SAMPLE_NOTE);
|
Note.showBanner(SAMPLE_NOTE);
|
||||||
expect(document.querySelector('.note-banner .btn.btn-danger')).not.toBeNull();
|
expect(document.querySelector('.note-banner .btn.btn-cancel')).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||||
@@ -106,7 +106,7 @@ describe('Note.showBanner', () => {
|
|||||||
|
|
||||||
it('T9: clicking the NVM button removes the banner from the DOM', () => {
|
it('T9: clicking the NVM button removes the banner from the DOM', () => {
|
||||||
Note.showBanner(SAMPLE_NOTE);
|
Note.showBanner(SAMPLE_NOTE);
|
||||||
document.querySelector('.note-banner .btn.btn-danger').click();
|
document.querySelector('.note-banner .btn.btn-cancel').click();
|
||||||
expect(document.querySelector('.note-banner')).toBeNull();
|
expect(document.querySelector('.note-banner')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,55 +52,80 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
|
@media (min-width: 900px) { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
@media (min-width: 1200px) { grid-template-columns: repeat(4, 1fr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-item {
|
.note-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: 0.4rem;
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: rgba(var(--priUser), 0.06);
|
background-color: rgba(var(--tooltip-bg), 0.75);
|
||||||
border: 1px solid rgba(var(--priUser), 0.2);
|
backdrop-filter: blur(6px);
|
||||||
border-radius: 4px;
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
width: 14rem;
|
border-radius: 0.5rem;
|
||||||
|
cursor: help;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&.note-item--active {
|
||||||
|
border-color: rgba(var(--terUser), 1);
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
|
||||||
|
|
||||||
|
.note-item__title { color: rgba(var(--terUser), 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-item__body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.note-item__title {
|
.note-item__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-item__description {
|
.note-item__description {
|
||||||
margin: 0;
|
margin: 0.25rem 0 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image box — must have a defined size so Selenium can interact with it.
|
// Image box — right-side; must have a defined size so Selenium can interact with it.
|
||||||
.note-item__image-box {
|
.note-item__image-box {
|
||||||
width: 5rem;
|
width: 3rem;
|
||||||
height: 5rem;
|
height: 3rem;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(var(--priUser), 0.12);
|
background: rgba(var(--priUser), 0.12);
|
||||||
border: 1px dashed rgba(var(--priUser), 0.4);
|
border: 1px dashed rgba(var(--priUser), 0.7);
|
||||||
border-radius: 2px;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|
||||||
&:hover { opacity: 1; }
|
&:hover { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlocked palette swatch inside a note item
|
// Unlocked palette swatch (right side, same footprint as image-box)
|
||||||
.note-item__palette {
|
.note-item__palette {
|
||||||
width: 5rem;
|
width: 3rem;
|
||||||
height: 5rem;
|
height: 3rem;
|
||||||
|
flex-shrink: 0;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 2px solid rgba(var(--priUser), 0.4);
|
border: 2px solid rgba(var(--priUser), 0.4);
|
||||||
}
|
}
|
||||||
@@ -112,12 +137,13 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
background: var(--bg, #1a1a1a);
|
background-color: rgba(var(--tooltip-bg), 0.92);
|
||||||
border: 1px solid rgba(var(--priUser), 0.4);
|
backdrop-filter: blur(8px);
|
||||||
border-radius: 4px;
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
|
border-radius: 0.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
min-width: 12rem;
|
min-width: 10rem;
|
||||||
|
|
||||||
&:not([hidden]) { display: flex; flex-direction: column; }
|
&:not([hidden]) { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
@@ -130,12 +156,34 @@
|
|||||||
.note-swatch-body {
|
.note-swatch-body {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
border-radius: 2px;
|
border-radius: 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid rgba(var(--priUser), 0.3);
|
// Gradient uses vars scoped to the parent palette-* class,
|
||||||
|
// so each swatch shows its own palette's colours (same as .swatch).
|
||||||
|
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%
|
||||||
|
);
|
||||||
|
border: 0.15rem solid rgba(var(--secUser), 0.5);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
transition: border-color 0.12s, box-shadow 0.12s;
|
||||||
|
|
||||||
&:hover { border-color: rgba(var(--priUser), 0.8); }
|
&:hover {
|
||||||
|
border-color: rgba(var(--terUser), 1);
|
||||||
|
box-shadow: 0 0 6px rgba(var(--ninUser), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.previewing {
|
||||||
|
border: 0.2rem solid rgba(var(--ninUser), 1);
|
||||||
|
box-shadow: 0 0 0.75rem rgba(var(--ninUser), 0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,16 +200,25 @@
|
|||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Confirm submenu ────────────────────────────────────────────────────────
|
// ── Confirm submenu — floats below modal, out of its flow ─────────────────
|
||||||
|
|
||||||
.note-palette-confirm {
|
.note-palette-confirm {
|
||||||
border-top: 1px solid rgba(var(--priUser), 0.2);
|
position: absolute;
|
||||||
padding-top: 0.5rem;
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 201;
|
||||||
|
background-color: rgba(var(--tooltip-bg), 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
|
|
||||||
&:not([hidden]) { display: flex; flex-direction: column; }
|
&:not([hidden]) { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; }
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,23 @@
|
|||||||
<li class="note-item" data-slug="{{ item.obj.slug }}"
|
<li class="note-item" data-slug="{{ item.obj.slug }}"
|
||||||
data-set-palette-url="{% url 'billboard:note_set_palette' item.obj.slug %}">
|
data-set-palette-url="{% url 'billboard:note_set_palette' item.obj.slug %}">
|
||||||
|
|
||||||
|
<div class="note-item__body">
|
||||||
|
<p class="note-item__title">{{ item.title }}</p>
|
||||||
|
<p class="note-item__description">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if item.obj.palette %}
|
{% if item.obj.palette %}
|
||||||
<div class="note-item__palette {{ item.obj.palette }}"></div>
|
<div class="note-item__palette {{ item.obj.palette }}"></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="note-item__image-box">?</div>
|
<div class="note-item__image-box">?</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="note-item__title">{{ item.title }}</p>
|
|
||||||
<p class="note-item__description">{{ item.description }}</p>
|
|
||||||
|
|
||||||
{% if not item.obj.palette and item.palette_options %}
|
{% if not item.obj.palette and item.palette_options %}
|
||||||
<template class="note-palette-modal-tpl">
|
<template class="note-palette-modal-tpl">
|
||||||
<div class="note-palette-modal">
|
<div class="note-palette-modal">
|
||||||
{% for palette_name in item.palette_options %}
|
{% for palette_name in item.palette_options %}
|
||||||
<div class="{{ palette_name }}">
|
<div class="{{ palette_name }}">
|
||||||
<div class="note-swatch-body"></div>
|
<div class="note-swatch-body"></div>
|
||||||
<button type="button" class="btn btn-confirm">OK</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="note-palette-confirm" hidden>
|
<div class="note-palette-confirm" hidden>
|
||||||
|
|||||||
Reference in New Issue
Block a user