diff --git a/src/apps/applets/static/apps/applets/row-lock.js b/src/apps/applets/static/apps/applets/row-lock.js new file mode 100644 index 0000000..e1bbfdd --- /dev/null +++ b/src/apps/applets/static/apps/applets/row-lock.js @@ -0,0 +1,60 @@ +// Click-lock state for `.applet-list-entry.row-3col` rows on the dashboard +// applet grids (My Posts / My Buds / My Notes / My Scrolls / My Games). +// +// Hover styling is pure CSS (`.applet-list-entry.row-3col:hover`); this +// module just persists the same highlight on tap/click so touch devices can +// pin a row to read it, and mirrors the toggle-off / shift-to-other-row / +// click-outside-dismiss behaviour the note-page click-lock established. +// +// State machine (clicking …): +// • a row that's not locked → lock it (clearing any prior lock) +// • the currently-locked row again → unlock it +// • a different row → move the lock to that row +// • anywhere not inside a row → clear the lock + +(function () { + 'use strict'; + + var _lockedRow = null; + var _bound = false; + + function _clearLock() { + if (_lockedRow) { + _lockedRow.classList.remove('row-locked'); + _lockedRow = null; + } + } + + function _onClick(e) { + var row = e.target.closest('.applet-list-entry.row-3col'); + if (row) { + if (row === _lockedRow) { + _clearLock(); + } else { + _clearLock(); + row.classList.add('row-locked'); + _lockedRow = row; + } + return; + } + _clearLock(); + } + + function _init() { + if (_bound) return; + _bound = true; + document.addEventListener('click', _onClick); + } + + function _testReset() { + _clearLock(); + } + + window.RowLock = { + _init: _init, + _testReset: _testReset, + get _lockedRow() { return _lockedRow; }, + }; + + document.addEventListener('DOMContentLoaded', _init); +}()); diff --git a/src/static/tests/RowLockSpec.js b/src/static/tests/RowLockSpec.js new file mode 100644 index 0000000..9708d16 --- /dev/null +++ b/src/static/tests/RowLockSpec.js @@ -0,0 +1,68 @@ +describe("RowLock", () => { + let fixture, row1, row2; + + function makeRow(label) { + const li = document.createElement("li"); + li.className = "applet-list-entry row-3col"; + li.innerHTML = ` + ${label} + body + + `; + return li; + } + + beforeEach(() => { + RowLock._testReset(); + fixture = document.createElement("ul"); + fixture.className = "applet-list"; + row1 = makeRow("Row 1"); + row2 = makeRow("Row 2"); + fixture.appendChild(row1); + fixture.appendChild(row2); + document.body.appendChild(fixture); + RowLock._init(); + }); + + afterEach(() => { + RowLock._testReset(); + fixture.remove(); + }); + + it("clicking a row adds .row-locked", () => { + row1.click(); + expect(row1.classList.contains("row-locked")).toBe(true); + }); + + it("clicking the same row again clears the lock", () => { + row1.click(); + row1.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + }); + + it("clicking a different row moves the lock", () => { + row1.click(); + row2.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + expect(row2.classList.contains("row-locked")).toBe(true); + }); + + it("clicking outside any row clears the lock", () => { + row1.click(); + document.body.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + }); + + it("clicking a child element of the row still locks the row", () => { + const body = row1.querySelector(".row-body"); + body.click(); + expect(row1.classList.contains("row-locked")).toBe(true); + }); + + it("only one row carries .row-locked at a time", () => { + row1.click(); + row2.click(); + const locked = document.querySelectorAll(".applet-list-entry.row-3col.row-locked"); + expect(locked.length).toBe(1); + }); +}); diff --git a/src/static/tests/SpecRunner.html b/src/static/tests/SpecRunner.html index 93dcbf9..87819f0 100644 --- a/src/static/tests/SpecRunner.html +++ b/src/static/tests/SpecRunner.html @@ -28,7 +28,9 @@ + + diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index dea9a0f..622f11e 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -47,6 +47,10 @@ grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto); align-items: baseline; gap: 0.5rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + border-radius: 0.25rem; + transition: background-color 0.12s ease, color 0.12s ease; .row-title { white-space: nowrap; @@ -68,6 +72,23 @@ text-align: right; white-space: nowrap; } + + // Hover (mouse) / click-lock (touch + click persistence) — both states + // share the highlight. JS in `apps/applets/row-lock.js` toggles + // `.row-locked` on click; `:hover` is pure CSS. Background to --secUser, + // title to --quaUser, dimmed body + ts brought to full opacity in + // --priUser (the back-of-card colour, readable against the --secUser + // fill). + &:hover, + &.row-locked { + background-color: rgba(var(--secUser), 1); + + .row-title, + a.row-title { color: rgba(var(--quiUser), 1); text-shadow: none; } + + .row-body, + .row-ts { color: rgba(var(--priUser), 1); opacity: 1; } + } } .applet-list-entry { diff --git a/src/static_src/tests/RowLockSpec.js b/src/static_src/tests/RowLockSpec.js new file mode 100644 index 0000000..9708d16 --- /dev/null +++ b/src/static_src/tests/RowLockSpec.js @@ -0,0 +1,68 @@ +describe("RowLock", () => { + let fixture, row1, row2; + + function makeRow(label) { + const li = document.createElement("li"); + li.className = "applet-list-entry row-3col"; + li.innerHTML = ` + ${label} + body + + `; + return li; + } + + beforeEach(() => { + RowLock._testReset(); + fixture = document.createElement("ul"); + fixture.className = "applet-list"; + row1 = makeRow("Row 1"); + row2 = makeRow("Row 2"); + fixture.appendChild(row1); + fixture.appendChild(row2); + document.body.appendChild(fixture); + RowLock._init(); + }); + + afterEach(() => { + RowLock._testReset(); + fixture.remove(); + }); + + it("clicking a row adds .row-locked", () => { + row1.click(); + expect(row1.classList.contains("row-locked")).toBe(true); + }); + + it("clicking the same row again clears the lock", () => { + row1.click(); + row1.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + }); + + it("clicking a different row moves the lock", () => { + row1.click(); + row2.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + expect(row2.classList.contains("row-locked")).toBe(true); + }); + + it("clicking outside any row clears the lock", () => { + row1.click(); + document.body.click(); + expect(row1.classList.contains("row-locked")).toBe(false); + }); + + it("clicking a child element of the row still locks the row", () => { + const body = row1.querySelector(".row-body"); + body.click(); + expect(row1.classList.contains("row-locked")).toBe(true); + }); + + it("only one row carries .row-locked at a time", () => { + row1.click(); + row2.click(); + const locked = document.querySelectorAll(".applet-list-entry.row-3col.row-locked"); + expect(locked.length).toBe(1); + }); +}); diff --git a/src/static_src/tests/SpecRunner.html b/src/static_src/tests/SpecRunner.html index 93dcbf9..87819f0 100644 --- a/src/static_src/tests/SpecRunner.html +++ b/src/static_src/tests/SpecRunner.html @@ -28,7 +28,9 @@ + + diff --git a/src/templates/core/base.html b/src/templates/core/base.html index 997b27d..1767754 100644 --- a/src/templates/core/base.html +++ b/src/templates/core/base.html @@ -187,6 +187,7 @@ +