From e2040fda8fddbd3b4c043648cca1a73930fa20bc Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 13 May 2026 00:27:39 -0400 Subject: [PATCH] =?UTF-8?q?applet=20rows:=20hover=20+=20click-lock=20highl?= =?UTF-8?q?ight=20on=20every=20`.applet-list-entry.row-3col`=20(My=20Posts?= =?UTF-8?q?=20/=20My=20Buds=20/=20My=20Notes=20/=20My=20Scrolls=20/=20My?= =?UTF-8?q?=20Games)=20=E2=80=94=20bg=20shifts=20to=20--secUser,=20title?= =?UTF-8?q?=20to=20--quiUser=20(overriding=20the=20inherited=20--terUser?= =?UTF-8?q?=20link=20color=20+=20stripping=20the=20text-shadow=20the=20glo?= =?UTF-8?q?bal=20`.applet-list-entry=20a:hover`=20rule=20had=20been=20baki?= =?UTF-8?q?ng=20in),=20body=20+=20ts=20cells=20come=20up=20from=20their=20?= =?UTF-8?q?dimmed=200.6=20/=200.5=20opacity=20to=20full=20--priUser=20so?= =?UTF-8?q?=20the=20dim=20middle/right=20cols=20pop=20against=20the=20--se?= =?UTF-8?q?cUser=20fill;=20new=20`apps/applets/static/apps/applets/row-loc?= =?UTF-8?q?k.js`=20IIFE=20module=20owns=20the=20touch-persistence=20state?= =?UTF-8?q?=20machine=20(single=20`=5FlockedRow`=20ref,=20`.row-locked`=20?= =?UTF-8?q?class=20toggle):=20clicking=20a=20row=20not=20currently=20locke?= =?UTF-8?q?d=20=E2=86=92=20locks=20(clearing=20any=20prior=20lock);=20clic?= =?UTF-8?q?king=20the=20locked=20row=20again=20=E2=86=92=20unlocks;=20clic?= =?UTF-8?q?king=20another=20row=20=E2=86=92=20moves=20the=20lock=20to=20th?= =?UTF-8?q?e=20new=20row;=20clicking=20anywhere=20not=20inside=20a=20`.row?= =?UTF-8?q?-3col`=20=E2=86=92=20clears=20the=20lock=20=E2=80=94=20mirrors?= =?UTF-8?q?=20the=20note-page=20`notes-locked`=20click-lock=20state=20mach?= =?UTF-8?q?ine=20but=20lighter=20(no=20DON/DOFF,=20no=20greeting=20swap,?= =?UTF-8?q?=20no=20fetch),=20one=20document-level=20click=20listener=20bou?= =?UTF-8?q?nd=20once=20via=20`=5Fbound`=20re-entry=20guard=20so=20beforeEa?= =?UTF-8?q?ch=20`=5Finit()`=20calls=20in=20specs=20don't=20pile=20up=20han?= =?UTF-8?q?dlers;=20loaded=20globally=20via=20`base.html`=20next=20to=20`a?= =?UTF-8?q?pplets.js`=20since=20the=20rows=20render=20on=20both=20/billboa?= =?UTF-8?q?rd/=20+=20/gameboard/;=20padding-inline=200.5rem=20+=20border-r?= =?UTF-8?q?adius=200.25rem=20on=20the=20row=20container=20shrinks=20the=20?= =?UTF-8?q?highlight=20to=20a=20chip=20shape=20so=20hovered=20rows=20don't?= =?UTF-8?q?=20bleed=20all=20the=20way=20to=20the=20applet=20box=20edge;=20?= =?UTF-8?q?6=20Jasmine=20specs=20in=20`RowLockSpec.js`=20cover=20the=20fou?= =?UTF-8?q?r=20state-machine=20transitions=20+=20the=20"child=20element=20?= =?UTF-8?q?of=20row=20still=20locks=20the=20parent=20row"=20affordance=20(?= =?UTF-8?q?since=20the=20user=20can=20tap=20the=20body=20cell=20text,=20no?= =?UTF-8?q?t=20just=20the=20title=20link)=20+=20the=20"only=20one=20row=20?= =?UTF-8?q?carries=20.row-locked=20at=20a=20time"=20invariant;=20SpecRunne?= =?UTF-8?q?r.html=20updated=20(both=20static=5Fsrc=20+=20the=20static/=20r?= =?UTF-8?q?untime=20mirror=20the=20FT=20reads=20from=20per=20the=20project?= =?UTF-8?q?'s=20static-src=E2=86=92static=20copy=20discipline)=20=E2=80=94?= =?UTF-8?q?=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Opus 4.7 --- .../applets/static/apps/applets/row-lock.js | 60 ++++++++++++++++ src/static/tests/RowLockSpec.js | 68 +++++++++++++++++++ src/static/tests/SpecRunner.html | 2 + src/static_src/scss/_billboard.scss | 21 ++++++ src/static_src/tests/RowLockSpec.js | 68 +++++++++++++++++++ src/static_src/tests/SpecRunner.html | 2 + src/templates/core/base.html | 1 + 7 files changed, 222 insertions(+) create mode 100644 src/apps/applets/static/apps/applets/row-lock.js create mode 100644 src/static/tests/RowLockSpec.js create mode 100644 src/static_src/tests/RowLockSpec.js 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 @@ +