added type='button' to both guard portal btns so firefox won't normalize to type='submit'; fixed several FTs for new click-guard functionality on Role card select & room gear menu DEL & BYE btns; several restorations to landscape breakpoint incl. logged-ion display_name, copyright info; provided title to room_scroll.html; a slurry of other minor fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Disco DeDisco
2026-03-23 19:31:57 -04:00
parent eecb6c2be6
commit 5607f70852
15 changed files with 365 additions and 65 deletions

View File

@@ -105,7 +105,7 @@
}
// In landscape: shift gear btn and applet menus left of the footer right sidebar
@media (orientation: landscape) and (max-width: 1023px) {
@media (orientation: landscape) and (max-width: 1440px) {
$sidebar-w: 4rem;
.gameboard-page,

View File

@@ -173,8 +173,8 @@ body {
}
}
@media (orientation: landscape) and (max-width: 1023px) {
$sidebar-w: 4rem;
@media (orientation: landscape) and (max-width: 1440px) {
$sidebar-w: 5rem;
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
body {
@@ -198,34 +198,49 @@ body {
.container-fluid {
flex-direction: column;
height: 100%;
max-height: 700px;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
gap: 1rem;
padding: 0 0.25rem;
> form { margin-left: 0; flex-shrink: 0; order: -1; } // logout at top
> form { flex-shrink: 0; order: -1; } // logout above brand
}
.navbar-brand { order: 1; } // brand at bottom
.navbar-brand h1 {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 1rem;
font-size: 1.2rem;
line-height: 1.2;
white-space: nowrap;
margin-right: 2.75rem;
// margin-right: 3.25rem;
}
.navbar-brand {
order: 1; // brand at bottom
width: 100%;
display: flex;
justify-content: center;
}
.navbar-text,
.navbar-link { display: none; }
.navbar-text {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.65rem;
white-space: nowrap;
margin: auto 0;
.navbar-label { opacity: 0.7; }
}
.btn-primary {
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
margin-left: 0.75rem;
// margin-left: 0.75rem;
}
// Login form: email input can't fit in narrow sidebar
@@ -275,7 +290,7 @@ body {
flex-direction: column;
width: auto;
max-width: none;
gap: 1.5rem;
gap: 3rem;
a {
font-size: 1.75rem;
@@ -285,7 +300,16 @@ body {
}
}
.footer-container { display: none; }
.footer-container {
position: absolute;
bottom: 0.75rem;
text-align: center;
font-size: 0.55rem;
line-height: 1.4;
color: rgba(var(--secUser), 0.5);
br { display: block; }
}
}
}
@@ -293,15 +317,16 @@ body {
body .container {
.navbar {
padding: 0 0 0.25rem 0;
.navbar-brand h1 {
font-size: 1.2rem;
}
.btn-primary {
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
}
}
@@ -319,25 +344,25 @@ body {
}
}
@media (min-width: 1024px) and (max-height: 700px) {
body .container .navbar {
padding: 0.5rem 0;
// @media (min-width: 1024px) and (max-height: 700px) {
// body .container .navbar {
// padding: 0.5rem 0;
.navbar-brand h1 {
font-size: 1.4rem;
}
}
// .navbar-brand h1 {
// font-size: 1.4rem;
// }
// }
#id_footer {
height: 3.5rem;
padding: 0.7rem 1rem;
gap: 0.35rem;
// #id_footer {
// height: 3.5rem;
// padding: 0.7rem 1rem;
// gap: 0.35rem;
#id_footer_nav a {
font-size: 1.2rem;
}
}
}
// #id_footer_nav a {
// font-size: 1.2rem;
// }
// }
// }
#id_footer {
flex-shrink: 0;
@@ -386,9 +411,41 @@ body {
}
.footer-container {
br { display: none; }
small {
font-size: 0.7rem;
opacity: 0.6;
}
}
}
#id_guard_portal {
display: none;
position: fixed;
z-index: 10000;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background-color: rgba(var(--tooltip-bg), 0.5);
backdrop-filter: blur(6px);
border: 0.1rem solid rgba(var(--secUser), 0.4);
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.4);
&.active {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.guard-message {
font-size: 0.85rem;
color: rgba(var(--secUser), 0.9);
text-align: center;
}
.guard-actions {
display: flex;
gap: 0.5rem;
}
}

View File

@@ -3,7 +3,7 @@
bottom: 0.5rem;
right: 0.5rem;
@media (orientation: landscape) and (max-width: 1023px) {
@media (orientation: landscape) and (max-width: 1440px) {
right: calc(4rem + 0.5rem);
bottom: 0.75rem;
top: auto;

View File

@@ -13,11 +13,15 @@ describe("RoleSelect", () => {
window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true })
);
// Default stub: auto-confirm so existing card-click tests pass unchanged.
// The click-guard integration describe overrides this with a capturing spy.
window.showGuard = (_anchor, _msg, onConfirm) => onConfirm && onConfirm();
});
afterEach(() => {
RoleSelect.closeFan();
testDiv.remove();
delete window.showGuard;
});
// ------------------------------------------------------------------ //
@@ -237,4 +241,145 @@ describe("RoleSelect", () => {
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
});
// ------------------------------------------------------------------ //
// click-guard integration //
// ------------------------------------------------------------------ //
// NOTE: cascade prevention (outside-click on backdrop not closing the //
// fan while the guard is active) relies on the guard portal's capture- //
// phase stopPropagation, which lives in base.html and requires //
// integration testing. The callback contract is fully covered below. //
// ------------------------------------------------------------------ //
describe("click-guard integration", () => {
let guardAnchor, guardMessage, guardConfirm, guardDismiss;
beforeEach(() => {
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
(anchor, message, onConfirm, onDismiss) => {
guardAnchor = anchor;
guardMessage = message;
guardConfirm = onConfirm;
guardDismiss = onDismiss;
}
);
RoleSelect.openFan();
});
describe("clicking a card", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
});
it("calls window.showGuard", () => {
expect(window.showGuard).toHaveBeenCalled();
});
it("passes the card element as the anchor", () => {
expect(guardAnchor).toBe(card);
});
it("message contains the role name", () => {
const roleName = card.querySelector(".card-role-name").textContent.trim();
expect(guardMessage).toContain(roleName);
});
it("message contains the role code", () => {
expect(guardMessage).toContain(card.dataset.role);
});
it("message contains a <br>", () => {
expect(guardMessage).toContain("<br>");
});
it("does not immediately close the fan", () => {
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("does not immediately POST to the select_role URL", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("adds .flipped to the card", () => {
expect(card.classList.contains("flipped")).toBe(true);
});
it("adds .guard-active to the card", () => {
expect(card.classList.contains("guard-active")).toBe(true);
});
it("mouseleave does not remove .flipped while guard is active", () => {
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(true);
});
});
describe("confirming the guard (OK)", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
guardConfirm();
});
it("removes .guard-active from the card", () => {
expect(card.classList.contains("guard-active")).toBe(false);
});
it("closes the fan", () => {
expect(document.querySelector(".role-select-backdrop")).toBeNull();
});
it("POSTs to the select_role URL", () => {
expect(window.fetch).toHaveBeenCalledWith(
"/epic/room/test-uuid/select-role",
jasmine.objectContaining({ method: "POST" })
);
});
it("appends a .card to #id_inv_role_card", () => {
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
});
});
describe("dismissing the guard (NVM or outside click)", () => {
let card;
beforeEach(() => {
card = document.querySelector("#id_role_select .card");
card.click();
guardDismiss();
});
it("removes .guard-active from the card", () => {
expect(card.classList.contains("guard-active")).toBe(false);
});
it("removes .flipped from the card", () => {
expect(card.classList.contains("flipped")).toBe(false);
});
it("leaves the fan open", () => {
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
});
it("does not POST to the select_role URL", () => {
expect(window.fetch).not.toHaveBeenCalled();
});
it("does not add a card to inventory", () => {
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
});
it("restores normal mouseleave behaviour on the card", () => {
card.dispatchEvent(new MouseEvent("mouseenter"));
card.dispatchEvent(new MouseEvent("mouseleave"));
expect(card.classList.contains("flipped")).toBe(false);
});
});
});
});