tray cards: shadow, hover-tilt w. focus persistence, role-card tooltip — TDD

- _tray.scss: drop-shadow on cell child elements (img → filter:drop-shadow so the silhouette is the shadow caster, div → box-shadow); 7° hover-tilt on .tray-role-card > img (-7°) and .tray-sig-card > .sig-stage-card (+7° via the standalone `rotate` property so the existing -5° baseline transform composes); :focus persists the tilt after click; cursor: pointer
- tray.js: set tabIndex=0 on placeCard's role cell + on template-rendered .tray-role-card / .tray-sig-card cells at init() so :focus latches the hover state; clear tabindex in reset() for Jasmine afterEach
- TraySpec: 4 new specs covering placeCard tabindex, reset cleanup, init-time tabindex on template-rendered sig & role cards, no-tabindex on bare cells
- New tray-tooltip.js (#id_tooltip_portal) — Phase 1 of the apps.tooltips integration: hovering .tray-role-card > img copies its sibling .tt's innerHTML into the page-root portal, anchors above/below the trigger, & clamps to the viewport horizontally; mousemove outside the union of [trigger, portal] rects clears the portal (Game-Kit pattern, no btns)
- room.html: #id_tooltip_portal mounted at room-page root (outside tray's overflow:hidden); .tt block rendered inline inside .tray-role-card via {% tooltip %} templatetag w. title=role display name & description="[Placeholder description]"
- epic/views.py: my_tray_role_tooltip context dict ({title, description}) keyed off the seated role
- TrayTooltipSpec: 8 specs covering portal population, .active class, sibling-.tt fallback, viewport-edge clamp left/right, and union-rect mouseleave
- 2 FTs in test_component_tray_tooltip.py: hover role img → portal title=Player + description=Placeholder; mouseleave → portal clears

Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel via a DRY refactor) deferred per plan.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-03 18:40:10 -04:00
parent 75fcc5b34d
commit 08243d109d
12 changed files with 849 additions and 10 deletions

View File

@@ -470,6 +470,11 @@ describe("Tray", () => {
expect(firstCell.dataset.role).toBe("NC");
});
it("sets tabIndex=0 on the placed cell so :focus persists the hover-tilt", () => {
Tray.placeCard("PC", null);
expect(firstCell.tabIndex).toBe(0);
});
it("grid cell count stays at 8", () => {
Tray.placeCard("PC", null);
expect(grid.children.length).toBe(8);
@@ -518,5 +523,57 @@ describe("Tray", () => {
expect(firstCell.classList.contains("tray-role-card")).toBe(false);
expect(firstCell.dataset.role).toBeUndefined();
});
it("reset() also clears tabindex from the placed cell", () => {
Tray.placeCard("PC", null);
Tray.reset();
expect(firstCell.hasAttribute("tabindex")).toBe(false);
});
});
// ---------------------------------------------------------------------- //
// init() — focusable tray cards //
// ---------------------------------------------------------------------- //
//
// .tray-sig-card is rendered server-side by room.html when the seat has a
// significator; .tray-role-card may be too if the seat already has a role.
// init() must mark these cells tabbable so the SCSS :focus rule persists
// the hover-tilt animation after the user clicks the card.
describe("init() — focusable tray cards", () => {
let grid;
beforeEach(() => {
grid = document.createElement("div");
grid.id = "id_tray_grid";
document.body.appendChild(grid);
});
afterEach(() => grid.remove());
function _addCell(extraClass) {
const cell = document.createElement("div");
cell.className = "tray-cell" + (extraClass ? " " + extraClass : "");
grid.appendChild(cell);
return cell;
}
it("sets tabIndex=0 on a template-rendered .tray-sig-card", () => {
const sigCell = _addCell("tray-sig-card");
Tray.init();
expect(sigCell.tabIndex).toBe(0);
});
it("sets tabIndex=0 on a template-rendered .tray-role-card", () => {
const roleCell = _addCell("tray-role-card");
Tray.init();
expect(roleCell.tabIndex).toBe(0);
});
it("does NOT set tabindex on bare .tray-cell elements", () => {
const empty = _addCell();
Tray.init();
expect(empty.hasAttribute("tabindex")).toBe(false);
});
});
});