diff --git a/src/apps/billboard/static/apps/billboard/note-page.js b/src/apps/billboard/static/apps/billboard/note-page.js
index 0ae9cd3..bfb4d24 100644
--- a/src/apps/billboard/static/apps/billboard/note-page.js
+++ b/src/apps/billboard/static/apps/billboard/note-page.js
@@ -5,6 +5,8 @@
var _activeItem = null;
var _originalPalette = null;
var _dismissTimer = null;
+ var _lockedItem = null; // click-locked note (glow + DON/DOFF pinned)
+ var _donnedItem = null; // currently DONned note (persistent glow)
// ── helpers ──────────────────────────────────────────────────────────────
@@ -47,6 +49,23 @@
if (el) el.style.display = 'none';
}
+ // ── lock helpers ──────────────────────────────────────────────────────────
+
+ function _clearLock() {
+ if (_lockedItem) {
+ _lockedItem.classList.remove('note-item--locked');
+ _lockedItem = null;
+ }
+ document.body.classList.remove('notes-locked');
+ }
+
+ function _setGreeting(greeting, name) {
+ var prefix = document.getElementById('id_greeting_prefix');
+ var nameEl = document.getElementById('id_greeting_name');
+ if (prefix) prefix.innerHTML = greeting;
+ if (nameEl) nameEl.textContent = name;
+ }
+
// ── modal lifecycle ───────────────────────────────────────────────────────
function _openModal() {
@@ -88,47 +107,31 @@
_originalPalette = null;
}
- // Wire event listeners onto the freshly-cloned modal DOM.
function _wireModal() {
var modal = _activeModal();
if (!modal) return;
- // Swatch body click → preview palette sitewide + show confirm
modal.querySelectorAll('.note-swatch-body').forEach(function (body) {
body.addEventListener('click', function (e) {
e.stopPropagation();
if (_selectedPalette) _revertPreview();
-
_selectedPalette = _paletteClass(body.parentElement);
_originalPalette = _currentBodyPalette();
-
body.classList.add('previewing');
_swapBodyPalette(_selectedPalette);
_showConfirm(modal);
-
- _dismissTimer = setTimeout(function () {
- _revertPreview();
- }, 10000);
+ _dismissTimer = setTimeout(function () { _revertPreview(); }, 10000);
});
});
- // Confirm OK → commit palette sitewide
modal.querySelectorAll('.note-palette-confirm .btn.btn-confirm').forEach(function (btn) {
- btn.addEventListener('click', function (e) {
- e.stopPropagation();
- _doSetPalette();
- });
+ btn.addEventListener('click', function (e) { e.stopPropagation(); _doSetPalette(); });
});
- // 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();
- _revertPreview();
- });
+ btn.addEventListener('click', function (e) { e.stopPropagation(); _revertPreview(); });
});
- // Stop modal clicks from reaching the body dismiss handler.
modal.addEventListener('click', function (e) { e.stopPropagation(); });
}
@@ -138,18 +141,13 @@
var url = _activeItem.dataset.setPaletteUrl;
var palette = _selectedPalette;
var item = _activeItem;
- // 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',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': _getCsrf(),
- },
+ method: 'POST', credentials: 'same-origin',
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': _getCsrf() },
body: JSON.stringify({ palette: palette }),
})
.then(function (r) { return r.json(); })
@@ -171,12 +169,7 @@
});
}
- // ── init ──────────────────────────────────────────────────────────────────
-
- function _setGreeting(name) {
- var el = document.getElementById('id_greeting_name');
- if (el) el.textContent = name;
- }
+ // ── DON/DOFF ──────────────────────────────────────────────────────────────
function _bindDonDoff(item) {
var donBtn = item.querySelector('.note-don-btn');
@@ -192,11 +185,21 @@
})
.then(function (r) { return r.json(); })
.then(function (data) {
- _setGreeting(data.title);
- donBtn.classList.add('btn-disabled');
- donBtn.textContent = '×';
- doffBtn.classList.remove('btn-disabled');
- doffBtn.textContent = 'DOFF';
+ // Auto-DOFF any previously DONned note (UI only — backend replaces active_title)
+ if (_donnedItem && _donnedItem !== item) {
+ _donnedItem.classList.remove('note-item--donned');
+ var prevDon = _donnedItem.querySelector('.note-don-btn');
+ var prevDoff = _donnedItem.querySelector('.note-doff-btn');
+ if (prevDon) { prevDon.classList.remove('btn-disabled'); prevDon.textContent = 'DON'; }
+ if (prevDoff) { prevDoff.classList.add('btn-disabled'); prevDoff.textContent = '×'; }
+ }
+ _donnedItem = item;
+ item.classList.add('note-item--donned');
+ // Clear lock so hover is restored for other notes
+ _clearLock();
+ donBtn.classList.add('btn-disabled'); donBtn.textContent = '×';
+ doffBtn.classList.remove('btn-disabled'); doffBtn.textContent = 'DOFF';
+ _setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman');
});
});
@@ -207,33 +210,60 @@
method: 'POST', credentials: 'same-origin',
headers: { 'X-CSRFToken': _getCsrf() },
})
- .then(function () {
- _setGreeting('Earthman');
- doffBtn.classList.add('btn-disabled');
- doffBtn.textContent = '×';
- donBtn.classList.remove('btn-disabled');
- donBtn.textContent = 'DON';
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ _donnedItem = null;
+ item.classList.remove('note-item--donned');
+ _clearLock();
+ doffBtn.classList.add('btn-disabled'); doffBtn.textContent = '×';
+ donBtn.classList.remove('btn-disabled'); donBtn.textContent = 'DON';
+ _setGreeting(data.greeting || 'Welcome,', data.title || 'Earthman');
});
});
}
+ // ── init ──────────────────────────────────────────────────────────────────
+
function _init() {
- document.querySelectorAll('.note-item__image-box').forEach(function (box) {
- box.addEventListener('click', function (e) {
+ document.querySelectorAll('.note-item').forEach(function (item) {
+ // Detect already-DONned note on load (DON btn is disabled = currently equipped)
+ var don = item.querySelector('.note-don-btn');
+ if (don && don.classList.contains('btn-disabled')) {
+ item.classList.add('note-item--donned');
+ _donnedItem = item;
+ }
+
+ _bindDonDoff(item);
+
+ // Image box click → palette modal (for notes that have one)
+ var box = item.querySelector('.note-item__image-box:not(.note-item__image-box--label)');
+ if (box) {
+ box.addEventListener('click', function (e) {
+ e.stopPropagation();
+ _activeItem = item;
+ _openModal();
+ });
+ }
+
+ // Note click → toggle lock
+ item.addEventListener('click', function (e) {
e.stopPropagation();
- _activeItem = box.closest('.note-item');
- _openModal();
+ if (_lockedItem === item) {
+ _clearLock();
+ } else {
+ _clearLock();
+ _lockedItem = item;
+ item.classList.add('note-item--locked');
+ document.body.classList.add('notes-locked');
+ }
});
});
- document.querySelectorAll('.note-item').forEach(function (item) {
- _bindDonDoff(item);
- });
-
- // Body click → dismiss modal and revert any preview
+ // Body click → dismiss modal and clear lock
document.body.addEventListener('click', function () {
if (_selectedPalette) _revertPreview();
_closeModal();
+ _clearLock();
});
}
@@ -242,4 +272,20 @@
} else {
_init();
}
+
+ // Expose test API
+ window.NotePage = {
+ _init: _init,
+ _testReset: function () {
+ _selectedPalette = null;
+ _activeItem = null;
+ _originalPalette = null;
+ _dismissTimer = null;
+ _lockedItem = null;
+ _donnedItem = null;
+ document.body.classList.remove('notes-locked');
+ },
+ get _donnedItem() { return _donnedItem; },
+ set _donnedItem(v) { _donnedItem = v; },
+ };
}());
diff --git a/src/apps/billboard/views.py b/src/apps/billboard/views.py
index 9606088..5bab098 100644
--- a/src/apps/billboard/views.py
+++ b/src/apps/billboard/views.py
@@ -116,13 +116,13 @@ _NOTE_META = {
"swatch_label": None,
},
"super-schizo": {
- "title": "Schizoid Man",
+ "title": "Super-Schizo",
"description": mark_safe('Admin access granted to I. The Schizo as Significator'),
"palette_options": [],
"swatch_label": "I",
},
"super-nomad": {
- "title": "Stranger",
+ "title": "Super-Nomad",
"description": mark_safe('Admin access granted to 0. The Nomad as Significator'),
"palette_options": [],
"swatch_label": "0",
@@ -156,13 +156,14 @@ def my_notes(request):
active_title = request.user.active_title
note_items = [
{
- "obj": n,
- "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", []),
- "swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
- "palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
- "is_equipped": active_title is not None and active_title.pk == n.pk,
+ "obj": n,
+ "title": _NOTE_META.get(n.slug, {}).get("title", n.slug),
+ "recognition_title": n.display_title,
+ "description": _NOTE_META.get(n.slug, {}).get("description", ""),
+ "palette_options": _NOTE_META.get(n.slug, {}).get("palette_options", []),
+ "swatch_label": _NOTE_META.get(n.slug, {}).get("swatch_label"),
+ "palette_label": _PALETTE_LABELS.get(n.palette, "") if n.palette else "",
+ "is_equipped": active_title is not None and active_title.pk == n.pk,
}
for n in qs
]
@@ -183,8 +184,7 @@ def don_title(request, slug):
if request.method == "POST":
request.user.active_title = note
request.user.save(update_fields=["active_title"])
- title = _NOTE_META.get(slug, {}).get("title", slug.capitalize())
- return JsonResponse({"title": title})
+ return JsonResponse({"title": note.display_title, "greeting": note.display_greeting})
@login_required(login_url="/")
@@ -192,7 +192,7 @@ def doff_title(request, slug):
if request.method == "POST":
request.user.active_title = None
request.user.save(update_fields=["active_title"])
- return JsonResponse({"ok": True})
+ return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"})
@login_required(login_url="/")
diff --git a/src/static/tests/NotePageSpec.js b/src/static/tests/NotePageSpec.js
new file mode 100644
index 0000000..ec686c1
--- /dev/null
+++ b/src/static/tests/NotePageSpec.js
@@ -0,0 +1,204 @@
+function flushPromises() {
+ return new Promise(resolve => setTimeout(resolve, 0));
+}
+
+describe("NotePage", () => {
+ let testDiv, item1, item2, don1, doff1, don2, doff2;
+
+ function makeItem(slug, isEquipped) {
+ const li = document.createElement("li");
+ li.className = "note-item";
+ li.dataset.slug = slug;
+ li.dataset.donUrl = `/billboard/note/${slug}/don`;
+ li.dataset.doffUrl = `/billboard/note/${slug}/doff`;
+ li.innerHTML = `
+
+
+
+
+
+ `;
+ return li;
+ }
+
+ beforeEach(() => {
+ NotePage._testReset();
+ testDiv = document.createElement("div");
+ testDiv.innerHTML = `Welcome,Earthman`;
+ item1 = makeItem("super-schizo", false);
+ item2 = makeItem("super-nomad", false);
+ testDiv.appendChild(item1);
+ testDiv.appendChild(item2);
+ document.body.appendChild(testDiv);
+
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ title: "Schizoid Man", greeting: "21st Century" }) })
+ );
+
+ don1 = item1.querySelector(".note-don-btn");
+ doff1 = item1.querySelector(".note-doff-btn");
+ don2 = item2.querySelector(".note-don-btn");
+ doff2 = item2.querySelector(".note-doff-btn");
+
+ NotePage._init();
+ });
+
+ afterEach(() => {
+ testDiv.remove();
+ document.body.classList.remove("notes-locked");
+ delete window.fetch;
+ });
+
+ // ── Lock / unlock ─────────────────────────────────────────────────────────
+
+ describe("click-lock behaviour", () => {
+ it("clicking a note adds note-item--locked", () => {
+ item1.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(true);
+ });
+
+ it("clicking a note adds notes-locked to body", () => {
+ item1.click();
+ expect(document.body.classList.contains("notes-locked")).toBe(true);
+ });
+
+ it("clicking the same note again removes lock", () => {
+ item1.click();
+ item1.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+
+ it("clicking a different note moves lock to that note", () => {
+ item1.click();
+ item2.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(item2.classList.contains("note-item--locked")).toBe(true);
+ });
+
+ it("body click clears all locks", () => {
+ item1.click();
+ document.body.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+ });
+
+ // ── DON/DOFF state ────────────────────────────────────────────────────────
+
+ describe("DON button", () => {
+ it("clicking DON sends POST to don URL", async () => {
+ don1.click();
+ await flushPromises();
+ expect(window.fetch).toHaveBeenCalledWith(
+ "/billboard/note/super-schizo/don",
+ jasmine.objectContaining({ method: "POST" })
+ );
+ });
+
+ it("after DON, note gains note-item--donned", async () => {
+ don1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--donned")).toBe(true);
+ });
+
+ it("after DON, lock is cleared", async () => {
+ item1.click(); // lock first
+ don1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+
+ it("after DON, greeting prefix and name are updated", async () => {
+ don1.click();
+ await flushPromises();
+ expect(document.getElementById("id_greeting_prefix").innerHTML).toContain("21st Century");
+ expect(document.getElementById("id_greeting_name").textContent).toBe("Schizoid Man");
+ });
+
+ it("DONning item1 auto-removes donned state from previously DONned item2", async () => {
+ item2.classList.add("note-item--donned");
+ don2.classList.add("btn-disabled"); don2.textContent = "×";
+ doff2.classList.remove("btn-disabled"); doff2.textContent = "DOFF";
+ NotePage._donnedItem = item2;
+
+ don1.click();
+ await flushPromises();
+
+ expect(item2.classList.contains("note-item--donned")).toBe(false);
+ expect(don2.classList.contains("btn-disabled")).toBe(false);
+ expect(don2.textContent).toBe("DON");
+ expect(doff2.classList.contains("btn-disabled")).toBe(true);
+ });
+
+ it("DONning one does not POST a doff for the previous", async () => {
+ item2.classList.add("note-item--donned");
+ NotePage._donnedItem = item2;
+ don1.click();
+ await flushPromises();
+ // Only one fetch call (the DON), no DOFF call for item2
+ expect(window.fetch.calls.count()).toBe(1);
+ });
+ });
+
+ describe("DOFF button", () => {
+ it("clicking DOFF sends POST to doff URL", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ don1.classList.add("btn-disabled"); don1.textContent = "×";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(window.fetch).toHaveBeenCalledWith(
+ "/billboard/note/super-schizo/doff",
+ jasmine.objectContaining({ method: "POST" })
+ );
+ });
+
+ it("after DOFF, note loses note-item--donned", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--donned")).toBe(false);
+ });
+
+ it("after DOFF, greeting reverts to Welcome, / Earthman", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ document.getElementById("id_greeting_prefix").textContent = "21st Century";
+ document.getElementById("id_greeting_name").textContent = "Schizoid Man";
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(document.getElementById("id_greeting_prefix").textContent).toBe("Welcome,");
+ expect(document.getElementById("id_greeting_name").textContent).toBe("Earthman");
+ });
+ });
+
+ // ── Initial donned state ──────────────────────────────────────────────────
+
+ describe("initial load with an already-donned note", () => {
+ it("note whose DON is btn-disabled gets note-item--donned on init", () => {
+ const equippedItem = makeItem("stargazer", true);
+ testDiv.appendChild(equippedItem);
+ NotePage._testReset();
+ NotePage._init();
+ expect(equippedItem.classList.contains("note-item--donned")).toBe(true);
+ });
+ });
+});
diff --git a/src/static/tests/SpecRunner.html b/src/static/tests/SpecRunner.html
index e8bfc7d..63366ce 100644
--- a/src/static/tests/SpecRunner.html
+++ b/src/static/tests/SpecRunner.html
@@ -24,9 +24,11 @@
+
+
diff --git a/src/static_src/scss/_base.scss b/src/static_src/scss/_base.scss
index 61d0f53..7315e6c 100644
--- a/src/static_src/scss/_base.scss
+++ b/src/static_src/scss/_base.scss
@@ -596,3 +596,8 @@ body {
gap: 0.5rem;
}
}
+
+.card-ref {
+ color: rgba(var(--terUser), 1) !important;
+ font-weight: 600 !important;
+}
diff --git a/src/static_src/scss/_note.scss b/src/static_src/scss/_note.scss
index 0f1f027..e44ba9e 100644
--- a/src/static_src/scss/_note.scss
+++ b/src/static_src/scss/_note.scss
@@ -70,6 +70,8 @@
flex-direction: column;
gap: 0.4rem;
z-index: 1;
+ opacity: 0;
+ transition: opacity 0.15s;
.btn { margin: 0; }
}
@@ -85,19 +87,33 @@
backdrop-filter: blur(6px);
border: 0.1rem solid rgba(var(--secUser), 0.4);
border-radius: 0.5rem;
- cursor: help;
+ cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
- &:hover,
- &:active,
+ // Hover glow — only when no note is click-locked on the page
+ body:not(.notes-locked) &:hover {
+ border-color: rgba(var(--terUser), 1);
+ box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
+ .note-item__title { color: rgba(var(--terUser), 1); }
+ .note-don-doff { opacity: 1; }
+ }
+
+ // Palette modal open (existing)
&.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); }
}
+ // Click-locked: glow + DON/DOFF always visible
+ &.note-item--locked,
+ &.note-item--donned {
+ border-color: rgba(var(--terUser), 1);
+ box-shadow: 0 0 10px rgba(var(--ninUser), 0.35);
+ .note-item__title { color: rgba(var(--terUser), 1); }
+ .note-don-doff { opacity: 1; }
+ }
+
.note-item__body {
flex: 1;
min-width: 0;
@@ -168,11 +184,11 @@
&--label {
font-size: 1.1rem;
font-weight: bold;
- font-style: italic;
- color: rgba(var(--terUser), 1);
+ color: rgba(var(--secUser), 0.6);
opacity: 1;
cursor: default;
border-style: solid;
+ border-color: rgba(var(--secUser), 0.6);
&:hover { opacity: 1; }
}
}
diff --git a/src/static_src/tests/NotePageSpec.js b/src/static_src/tests/NotePageSpec.js
new file mode 100644
index 0000000..ec686c1
--- /dev/null
+++ b/src/static_src/tests/NotePageSpec.js
@@ -0,0 +1,204 @@
+function flushPromises() {
+ return new Promise(resolve => setTimeout(resolve, 0));
+}
+
+describe("NotePage", () => {
+ let testDiv, item1, item2, don1, doff1, don2, doff2;
+
+ function makeItem(slug, isEquipped) {
+ const li = document.createElement("li");
+ li.className = "note-item";
+ li.dataset.slug = slug;
+ li.dataset.donUrl = `/billboard/note/${slug}/don`;
+ li.dataset.doffUrl = `/billboard/note/${slug}/doff`;
+ li.innerHTML = `
+
+
+
+
+
+ `;
+ return li;
+ }
+
+ beforeEach(() => {
+ NotePage._testReset();
+ testDiv = document.createElement("div");
+ testDiv.innerHTML = `Welcome,Earthman`;
+ item1 = makeItem("super-schizo", false);
+ item2 = makeItem("super-nomad", false);
+ testDiv.appendChild(item1);
+ testDiv.appendChild(item2);
+ document.body.appendChild(testDiv);
+
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ title: "Schizoid Man", greeting: "21st Century" }) })
+ );
+
+ don1 = item1.querySelector(".note-don-btn");
+ doff1 = item1.querySelector(".note-doff-btn");
+ don2 = item2.querySelector(".note-don-btn");
+ doff2 = item2.querySelector(".note-doff-btn");
+
+ NotePage._init();
+ });
+
+ afterEach(() => {
+ testDiv.remove();
+ document.body.classList.remove("notes-locked");
+ delete window.fetch;
+ });
+
+ // ── Lock / unlock ─────────────────────────────────────────────────────────
+
+ describe("click-lock behaviour", () => {
+ it("clicking a note adds note-item--locked", () => {
+ item1.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(true);
+ });
+
+ it("clicking a note adds notes-locked to body", () => {
+ item1.click();
+ expect(document.body.classList.contains("notes-locked")).toBe(true);
+ });
+
+ it("clicking the same note again removes lock", () => {
+ item1.click();
+ item1.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+
+ it("clicking a different note moves lock to that note", () => {
+ item1.click();
+ item2.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(item2.classList.contains("note-item--locked")).toBe(true);
+ });
+
+ it("body click clears all locks", () => {
+ item1.click();
+ document.body.click();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+ });
+
+ // ── DON/DOFF state ────────────────────────────────────────────────────────
+
+ describe("DON button", () => {
+ it("clicking DON sends POST to don URL", async () => {
+ don1.click();
+ await flushPromises();
+ expect(window.fetch).toHaveBeenCalledWith(
+ "/billboard/note/super-schizo/don",
+ jasmine.objectContaining({ method: "POST" })
+ );
+ });
+
+ it("after DON, note gains note-item--donned", async () => {
+ don1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--donned")).toBe(true);
+ });
+
+ it("after DON, lock is cleared", async () => {
+ item1.click(); // lock first
+ don1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--locked")).toBe(false);
+ expect(document.body.classList.contains("notes-locked")).toBe(false);
+ });
+
+ it("after DON, greeting prefix and name are updated", async () => {
+ don1.click();
+ await flushPromises();
+ expect(document.getElementById("id_greeting_prefix").innerHTML).toContain("21st Century");
+ expect(document.getElementById("id_greeting_name").textContent).toBe("Schizoid Man");
+ });
+
+ it("DONning item1 auto-removes donned state from previously DONned item2", async () => {
+ item2.classList.add("note-item--donned");
+ don2.classList.add("btn-disabled"); don2.textContent = "×";
+ doff2.classList.remove("btn-disabled"); doff2.textContent = "DOFF";
+ NotePage._donnedItem = item2;
+
+ don1.click();
+ await flushPromises();
+
+ expect(item2.classList.contains("note-item--donned")).toBe(false);
+ expect(don2.classList.contains("btn-disabled")).toBe(false);
+ expect(don2.textContent).toBe("DON");
+ expect(doff2.classList.contains("btn-disabled")).toBe(true);
+ });
+
+ it("DONning one does not POST a doff for the previous", async () => {
+ item2.classList.add("note-item--donned");
+ NotePage._donnedItem = item2;
+ don1.click();
+ await flushPromises();
+ // Only one fetch call (the DON), no DOFF call for item2
+ expect(window.fetch.calls.count()).toBe(1);
+ });
+ });
+
+ describe("DOFF button", () => {
+ it("clicking DOFF sends POST to doff URL", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ don1.classList.add("btn-disabled"); don1.textContent = "×";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(window.fetch).toHaveBeenCalledWith(
+ "/billboard/note/super-schizo/doff",
+ jasmine.objectContaining({ method: "POST" })
+ );
+ });
+
+ it("after DOFF, note loses note-item--donned", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(item1.classList.contains("note-item--donned")).toBe(false);
+ });
+
+ it("after DOFF, greeting reverts to Welcome, / Earthman", async () => {
+ window.fetch = jasmine.createSpy("fetch").and.returnValue(
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ greeting: "Welcome,", title: "Earthman" }) })
+ );
+ document.getElementById("id_greeting_prefix").textContent = "21st Century";
+ document.getElementById("id_greeting_name").textContent = "Schizoid Man";
+ item1.classList.add("note-item--donned");
+ doff1.classList.remove("btn-disabled"); doff1.textContent = "DOFF";
+ NotePage._donnedItem = item1;
+
+ doff1.click();
+ await flushPromises();
+ expect(document.getElementById("id_greeting_prefix").textContent).toBe("Welcome,");
+ expect(document.getElementById("id_greeting_name").textContent).toBe("Earthman");
+ });
+ });
+
+ // ── Initial donned state ──────────────────────────────────────────────────
+
+ describe("initial load with an already-donned note", () => {
+ it("note whose DON is btn-disabled gets note-item--donned on init", () => {
+ const equippedItem = makeItem("stargazer", true);
+ testDiv.appendChild(equippedItem);
+ NotePage._testReset();
+ NotePage._init();
+ expect(equippedItem.classList.contains("note-item--donned")).toBe(true);
+ });
+ });
+});
diff --git a/src/static_src/tests/SpecRunner.html b/src/static_src/tests/SpecRunner.html
index e8bfc7d..63366ce 100644
--- a/src/static_src/tests/SpecRunner.html
+++ b/src/static_src/tests/SpecRunner.html
@@ -24,9 +24,11 @@
+
+
diff --git a/src/templates/apps/billboard/my_notes.html b/src/templates/apps/billboard/my_notes.html
index 93bd7ae..d54422a 100644
--- a/src/templates/apps/billboard/my_notes.html
+++ b/src/templates/apps/billboard/my_notes.html
@@ -25,11 +25,11 @@
{{ item.title }}
-
{{ item.description }}
+
{{ item.description|safe }}
- - Title: {{ item.title }}
+ - Title: {{ item.recognition_title }}
{% if item.obj.palette %}
- Palette: {{ item.palette_label }}
{% endif %}
diff --git a/src/templates/core/_partials/_navbar.html b/src/templates/core/_partials/_navbar.html
index 6eb505f..6d9956e 100644
--- a/src/templates/core/_partials/_navbar.html
+++ b/src/templates/core/_partials/_navbar.html
@@ -2,7 +2,7 @@