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 () {
|
||||
'use strict';
|
||||
|
||||
var _state = 'closed'; // 'closed' | 'open' | 'previewing'
|
||||
var _selectedPalette = null;
|
||||
var _activeItem = null;
|
||||
var _originalPalette = null;
|
||||
var _dismissTimer = null;
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -15,6 +16,22 @@
|
||||
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() {
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
@@ -23,7 +40,6 @@
|
||||
// ── modal lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
function _openModal() {
|
||||
// Clone from <template> if the modal is not in the DOM yet.
|
||||
var existing = _activeModal();
|
||||
if (!existing) {
|
||||
var tpl = _activeItem.querySelector('.note-palette-modal-tpl');
|
||||
@@ -32,15 +48,37 @@
|
||||
_activeItem.appendChild(clone);
|
||||
_wireModal();
|
||||
}
|
||||
_state = 'open';
|
||||
_activeItem.classList.add('note-item--active');
|
||||
var confirmEl = _activeModal().querySelector('.note-palette-confirm');
|
||||
if (confirmEl) confirmEl.hidden = true;
|
||||
}
|
||||
|
||||
function _closeModal() {
|
||||
_state = 'closed';
|
||||
clearTimeout(_dismissTimer);
|
||||
_dismissTimer = null;
|
||||
var modal = _activeModal();
|
||||
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.
|
||||
@@ -48,28 +86,30 @@
|
||||
var modal = _activeModal();
|
||||
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) {
|
||||
body.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
_selectedPalette = _paletteClass(body.parentElement);
|
||||
_state = 'previewing';
|
||||
modal.remove();
|
||||
});
|
||||
});
|
||||
// Clear any in-progress preview first
|
||||
if (_selectedPalette) _revertPreview();
|
||||
|
||||
_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');
|
||||
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) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
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) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
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(); });
|
||||
}
|
||||
|
||||
@@ -94,6 +134,8 @@
|
||||
function _doSetPalette() {
|
||||
var url = _activeItem.dataset.setPaletteUrl;
|
||||
var palette = _selectedPalette;
|
||||
var item = _activeItem;
|
||||
// Body already shows new palette from preview — keep it by not reverting.
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
@@ -106,7 +148,7 @@
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function () {
|
||||
_closeModal();
|
||||
var imageBox = _activeItem.querySelector('.note-item__image-box');
|
||||
var imageBox = item.querySelector('.note-item__image-box');
|
||||
if (imageBox) {
|
||||
var swatch = document.createElement('div');
|
||||
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 () {
|
||||
if (_state === 'previewing') {
|
||||
_openModal();
|
||||
}
|
||||
if (_selectedPalette) _revertPreview();
|
||||
_closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,17 @@ class NoteSetPaletteViewTest(TestCase):
|
||||
)
|
||||
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):
|
||||
def setUp(self):
|
||||
|
||||
@@ -110,14 +110,20 @@ _NOTE_META = {
|
||||
@login_required(login_url="/")
|
||||
def note_set_palette(request, slug):
|
||||
from django.http import Http404
|
||||
from apps.dashboard.views import _unlocked_palettes_for_user
|
||||
try:
|
||||
note = Note.objects.get(user=request.user, slug=slug)
|
||||
except Note.DoesNotExist:
|
||||
raise Http404
|
||||
if request.method == "POST":
|
||||
body = json.loads(request.body)
|
||||
note.palette = body.get("palette", "")
|
||||
palette = body.get("palette", "")
|
||||
note.palette = 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})
|
||||
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class StargazerNoteFromDashboardTest(FunctionalTest):
|
||||
# FYI navigates to Note page
|
||||
fyi.click()
|
||||
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
|
||||
@@ -172,61 +172,105 @@ class StargazerNoteFromDashboardTest(FunctionalTest):
|
||||
|
||||
# ── T2b ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_note_page_palette_modal_flow(self):
|
||||
"""Note page palette modal: image-box opens modal, swatch preview,
|
||||
body-click restores modal, OK raises confirm submenu, confirm sets palette."""
|
||||
Note.objects.create(
|
||||
user=self.gamer, slug="stargazer", earned_at=timezone.now(),
|
||||
)
|
||||
self.create_pre_authenticated_session("stargazer@test.io")
|
||||
def _open_modal_and_click_bardo(self):
|
||||
"""Helper: navigate to /billboard/my-notes/, open modal, click bardo swatch body.
|
||||
Returns (modal, confirm_menu) after the confirm bar is visible."""
|
||||
self.browser.get(self.live_server_url + "/billboard/my-notes/")
|
||||
|
||||
image_box = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-item__image-box")
|
||||
)
|
||||
|
||||
# Clicking ? opens palette modal
|
||||
image_box.click()
|
||||
modal = self.wait_for(
|
||||
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")
|
||||
self.browser.execute_script(
|
||||
"arguments[0].dispatchEvent(new MouseEvent('click', {bubbles: true}))",
|
||||
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(
|
||||
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
|
||||
confirm_menu.find_element(By.CSS_SELECTOR, ".btn.btn-confirm").click()
|
||||
def test_note_page_swatch_previews_palette_sitewide_and_ok_persists(self):
|
||||
"""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.browser.find_elements(By.CSS_SELECTOR, ".note-palette-modal")
|
||||
))
|
||||
item = self.browser.find_element(By.CSS_SELECTOR, ".note-item")
|
||||
self.assertTrue(
|
||||
item.find_elements(By.CSS_SELECTOR, ".note-item__palette.palette-bardo")
|
||||
self.assertTrue(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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -88,9 +88,9 @@ describe('Note.showBanner', () => {
|
||||
|
||||
// ── 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);
|
||||
expect(document.querySelector('.note-banner .btn.btn-danger')).not.toBeNull();
|
||||
expect(document.querySelector('.note-banner .btn.btn-cancel')).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── T8 ── FYI link ────────────────────────────────────────────────────────
|
||||
@@ -106,7 +106,7 @@ describe('Note.showBanner', () => {
|
||||
|
||||
it('T9: clicking the NVM button removes the banner from the DOM', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -52,55 +52,80 @@
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
|
||||
@media (min-width: 900px) { grid-template-columns: repeat(3, 1fr); }
|
||||
@media (min-width: 1200px) { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
|
||||
.note-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(var(--priUser), 0.06);
|
||||
border: 1px solid rgba(var(--priUser), 0.2);
|
||||
border-radius: 4px;
|
||||
width: 14rem;
|
||||
background-color: rgba(var(--tooltip-bg), 0.75);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
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 {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.note-item__description {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
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 {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(var(--priUser), 0.12);
|
||||
border: 1px dashed rgba(var(--priUser), 0.4);
|
||||
border-radius: 2px;
|
||||
border: 1px dashed rgba(var(--priUser), 0.7);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
|
||||
// Unlocked palette swatch inside a note item
|
||||
// Unlocked palette swatch (right side, same footprint as image-box)
|
||||
.note-item__palette {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 2px solid rgba(var(--priUser), 0.4);
|
||||
}
|
||||
@@ -112,12 +137,13 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
background: var(--bg, #1a1a1a);
|
||||
border: 1px solid rgba(var(--priUser), 0.4);
|
||||
border-radius: 4px;
|
||||
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.75rem;
|
||||
gap: 0.5rem;
|
||||
min-width: 12rem;
|
||||
min-width: 10rem;
|
||||
|
||||
&:not([hidden]) { display: flex; flex-direction: column; }
|
||||
|
||||
@@ -130,12 +156,34 @@
|
||||
.note-swatch-body {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 2px;
|
||||
border-radius: 0.25rem;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Confirm submenu ────────────────────────────────────────────────────────
|
||||
// ── Confirm submenu — floats below modal, out of its flow ─────────────────
|
||||
|
||||
.note-palette-confirm {
|
||||
border-top: 1px solid rgba(var(--priUser), 0.2);
|
||||
padding-top: 0.5rem;
|
||||
position: absolute;
|
||||
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;
|
||||
|
||||
&:not([hidden]) { display: flex; flex-direction: column; }
|
||||
&:not([hidden]) { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; }
|
||||
|
||||
p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@@ -12,22 +12,23 @@
|
||||
<li class="note-item" data-slug="{{ 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 %}
|
||||
<div class="note-item__palette {{ item.obj.palette }}"></div>
|
||||
{% else %}
|
||||
<div class="note-item__image-box">?</div>
|
||||
{% 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 %}
|
||||
<template class="note-palette-modal-tpl">
|
||||
<div class="note-palette-modal">
|
||||
{% for palette_name in item.palette_options %}
|
||||
<div class="{{ palette_name }}">
|
||||
<div class="note-swatch-body"></div>
|
||||
<button type="button" class="btn btn-confirm">OK</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="note-palette-confirm" hidden>
|
||||
|
||||
Reference in New Issue
Block a user