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 @@
+