applet rows: hover + click-lock highlight on every .applet-list-entry.row-3col (My Posts / My Buds / My Notes / My Scrolls / My Games) — bg shifts to --secUser, title to --quiUser (overriding the inherited --terUser link color + stripping the text-shadow the global .applet-list-entry a:hover rule had been baking in), body + ts cells come up from their dimmed 0.6 / 0.5 opacity to full --priUser so the dim middle/right cols pop against the --secUser fill; new apps/applets/static/apps/applets/row-lock.js IIFE module owns the touch-persistence state machine (single _lockedRow ref, .row-locked class toggle): clicking a row not currently locked → locks (clearing any prior lock); clicking the locked row again → unlocks; clicking another row → moves the lock to the new row; clicking anywhere not inside a .row-3col → clears the lock — mirrors the note-page notes-locked click-lock state machine but lighter (no DON/DOFF, no greeting swap, no fetch), one document-level click listener bound once via _bound re-entry guard so beforeEach _init() calls in specs don't pile up handlers; loaded globally via base.html next to applets.js since the rows render on both /billboard/ + /gameboard/; padding-inline 0.5rem + border-radius 0.25rem on the row container shrinks the highlight to a chip shape so hovered rows don't bleed all the way to the applet box edge; 6 Jasmine specs in RowLockSpec.js cover the four state-machine transitions + the "child element of row still locks the parent row" affordance (since the user can tap the body cell text, not just the title link) + the "only one row carries .row-locked at a time" invariant; SpecRunner.html updated (both static_src + the static/ runtime mirror the FT reads from per the project's static-src→static copy discipline) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
60
src/apps/applets/static/apps/applets/row-lock.js
Normal file
60
src/apps/applets/static/apps/applets/row-lock.js
Normal file
@@ -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);
|
||||||
|
}());
|
||||||
68
src/static/tests/RowLockSpec.js
Normal file
68
src/static/tests/RowLockSpec.js
Normal file
@@ -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 = `
|
||||||
|
<a class="row-title" href="#">${label}</a>
|
||||||
|
<span class="row-body">body</span>
|
||||||
|
<time class="row-ts">ts</time>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,7 +28,9 @@
|
|||||||
<script src="SkyWheelSpec.js"></script>
|
<script src="SkyWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
<script src="NotePageSpec.js"></script>
|
<script src="NotePageSpec.js"></script>
|
||||||
|
<script src="RowLockSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
|
<script src="/static/apps/applets/row-lock.js"></script>
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
<script src="/static/apps/billboard/note-page.js"></script>
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
|
|||||||
@@ -47,6 +47,10 @@
|
|||||||
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
|
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.5rem;
|
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 {
|
.row-title {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -68,6 +72,23 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
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 {
|
.applet-list-entry {
|
||||||
|
|||||||
68
src/static_src/tests/RowLockSpec.js
Normal file
68
src/static_src/tests/RowLockSpec.js
Normal file
@@ -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 = `
|
||||||
|
<a class="row-title" href="#">${label}</a>
|
||||||
|
<span class="row-body">body</span>
|
||||||
|
<time class="row-ts">ts</time>
|
||||||
|
`;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,7 +28,9 @@
|
|||||||
<script src="SkyWheelSpec.js"></script>
|
<script src="SkyWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
<script src="NotePageSpec.js"></script>
|
<script src="NotePageSpec.js"></script>
|
||||||
|
<script src="RowLockSpec.js"></script>
|
||||||
<!-- src files -->
|
<!-- src files -->
|
||||||
|
<script src="/static/apps/applets/row-lock.js"></script>
|
||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
<script src="/static/apps/billboard/note-page.js"></script>
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
|
|||||||
@@ -187,6 +187,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
||||||
<script src="{% static "apps/applets/applets.js" %}"></script>
|
<script src="{% static "apps/applets/applets.js" %}"></script>
|
||||||
|
<script src="{% static "apps/applets/row-lock.js" %}"></script>
|
||||||
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZone + '; path=/; SameSite=Lax';
|
document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZone + '; path=/; SameSite=Lax';
|
||||||
|
|||||||
Reference in New Issue
Block a user