role-select.js ensures Role select card stack disappears via WS upon conclusion of Role selection, w. if-conditional support from apps.epic.views; ensured border present on card-stack when .active in _room.scss; changed default #id_tray to unhidden, only hidden during Role select until Role selected; polished & unified Role .card-front, .card.back & .card-stack styling

This commit is contained in:
Disco DeDisco
2026-04-05 01:14:31 -04:00
parent c00288e256
commit bd3d7fc7bd
7 changed files with 87 additions and 24 deletions

View File

@@ -154,7 +154,7 @@ var RoleSelect = (function () {
var back = document.createElement("div");
back.className = "card-back";
back.textContent = "?";
back.textContent = "ROLE";
var front = document.createElement("div");
front.className = "card-front";
@@ -208,6 +208,10 @@ var RoleSelect = (function () {
function handleAllRolesFilled() {
var wrap = document.getElementById('id_pick_sigs_wrap');
if (wrap) wrap.style.display = '';
var stack = document.querySelector('.card-stack');
if (stack) stack.remove();
var trayWrap = document.getElementById('id_tray_wrap');
if (trayWrap) trayWrap.classList.remove('role-select-phase');
}
function handleSigSelectStarted() {

View File

@@ -7,7 +7,9 @@
if (!scene || !container) return;
var w = container.clientWidth, h = container.clientHeight;
if (!w || !h) return;
scene.style.transform = 'scale(' + Math.min(w / SCENE_W, h / SCENE_H) + ')';
var scale = Math.min(w / SCENE_W, h / SCENE_H);
scene.style.transform = 'scale(' + scale + ')';
document.documentElement.style.setProperty('--table-scale', scale);
}
if (document.readyState === 'loading') {

View File

@@ -762,6 +762,7 @@ class RoomViewAllRolesFilledTest(TestCase):
response = self.client.get(reverse("epic:room", kwargs={"room_id": self.room.id}))
parsed = self.lxml.fromstring(response.content)
[_] = parsed.cssselect("#id_pick_sigs_btn")
self.assertEqual(parsed.cssselect(".card-stack"), [])
def test_pick_sigs_btn_hidden_during_role_select(self):
# Clear one role — still mid-pick, wrap must be hidden

View File

@@ -189,6 +189,8 @@ def _role_select_context(room, user):
starter_roles = list(
room.table_seats.exclude(role__isnull=True).values_list("role", flat=True)
)
if len(starter_roles) == 6:
card_stack_state = None
_action_order = {r: i for i, r in enumerate(["PC", "NC", "EC", "SC", "AC", "BC"])}
assigned_seats = (
sorted(

View File

@@ -846,4 +846,53 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
"Tray should be closed after turn advances"
))
# ------------------------------------------------------------------ #
# Test 7 — PICK SIGS appears + card stack removed on last role #
# ------------------------------------------------------------------ #
def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
"""When the sixth and final role is confirmed, the all_roles_filled
WS event makes the PICK SIGS button visible and removes the card
stack from the DOM entirely."""
emails = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Last Role Test", owner=founder)
_fill_room_via_orm(room, emails)
room.table_status = Room.ROLE_SELECT
room.save()
# Pre-assign 5 roles (slots 26); founder (slot 1) is the final picker.
pre_assigned = {2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room,
gamer=slot.gamer,
slot_number=slot.slot_number,
role=pre_assigned.get(slot.slot_number),
)
room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
))
# Founder picks the last remaining role (PC — the only card in the fan).
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
self.confirm_guard()
# PICK SIGS wrap must become visible via the all_roles_filled WS event.
self.wait_for(lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
))
# Card stack must be removed from the DOM entirely.
self.wait_for(lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack")), 0,
))

View File

@@ -624,12 +624,25 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
background: rgba(var(--terUser), 1);
cursor: default;
transition: box-shadow 0.2s ease;
position: relative;
&::before {
content: "ROLE";
font-size: 0.6rem;
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1);
}
.fa-ban {
position: absolute;
font-size: 1.4rem;
}
&[data-state="eligible"] {
cursor: pointer;
border-color: rgba(var(--terUser), 1);
border: 2px solid rgba(var(--quiUser), 1);
box-shadow:
0 0 0.6rem rgba(var(--ninUser), 0.6),
0 0 0.6rem rgba(var(--ninUser), 1),
0 0 1.6rem rgba(var(--secUser), 0.25);
}
@@ -640,10 +653,11 @@ html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: aut
}
// ─── Card dimensions ───────────────────────────────────────────────────────
// Role cards are landscape format — wider than tall — and the largest card type.
// Sig cards (half this size) will be layered on top during SIG_SELECT.
$card-w: 160px;
$card-h: 110px;
// Base size matches the card-stack footprint; --table-scale (set by scaleTable()
// in room.js) stretches both the grid and individual cards to stay in sync with
// the scene transform. Fallback of 1 keeps the fan functional if JS hasn't run.
$card-w: 90px;
$card-h: 60px;
// ─── Role select modal ─────────────────────────────────────────────────────
@@ -662,27 +676,17 @@ $card-h: 110px;
#id_role_select {
// Always a 3×2 grid — 6 landscape cards in a row would overflow any viewport.
display: grid;
grid-template-columns: repeat(3, $card-w);
grid-template-columns: repeat(3, calc(#{$card-w} * var(--table-scale, 1)));
gap: 1rem;
pointer-events: none;
// Narrow portrait: scale cards down so the 3-col grid still fits
@media (max-width: 600px) {
grid-template-columns: repeat(3, 110px);
gap: 0.75rem;
.card {
width: 110px;
height: 75px;
}
}
}
// ─── Card component ────────────────────────────────────────────────────────
.card {
width: $card-w;
height: $card-h;
width: calc(#{$card-w} * var(--table-scale, 1));
height: calc(#{$card-h} * var(--table-scale, 1));
border-radius: 6px;
cursor: pointer;
pointer-events: auto;
@@ -707,7 +711,8 @@ $card-h: 110px;
.card-back {
transform: rotateY(0deg);
font-size: 1.5rem;
font-size: calc(0.66rem * var(--table-scale, 1));
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1);
background: rgba(var(--terUser), 1);
border: 2px solid rgba(var(--quiUser), 1);
@@ -719,7 +724,7 @@ $card-h: 110px;
text-align: center;
.card-role-name {
font-size: 0.75rem;
font-size: calc(0.66rem * var(--table-scale, 1));
color: rgba(var(--quaUser), 1);
text-transform: uppercase;
letter-spacing: 0.05em;

View File

@@ -95,7 +95,7 @@
{% include "apps/gameboard/_partials/_gatekeeper.html" %}
{% endif %}
{% if room.table_status %}
<div id="id_tray_wrap"{% if room.table_status == "ROLE_SELECT" %} class="role-select-phase"{% endif %}>
<div id="id_tray_wrap"{% if room.table_status == "ROLE_SELECT" and starter_roles|length < 6 %} class="role-select-phase"{% endif %}>
<div id="id_tray_handle">
<div id="id_tray_grip"></div>
<button id="id_tray_btn" aria-label="Open seat tray">