hex position indicators: chair icons at hex edge midpoints replace gate-slot circles

- Split .gate-overlay into .gate-backdrop (z-100, blur) + .gate-overlay modal (z-120) so .table-position elements (z-110) render above backdrop but below modal
- New _table_positions.html partial: 6 .table-position divs with .fa-chair, role label, and .fa-ban/.fa-circle-check status icons; included unconditionally in room.html
- New epic:room view at /gameboard/room/<uuid>/; gatekeeper redirects there when table_status set; pick_roles redirects there
- role-select.js: adds .active glow to position on selectRole(); swaps .fa-ban→.fa-circle-check in placeCard onComplete; handleTurnChanged clears stale .active from all positions
- FTs: PositionIndicatorsTest (5 tests) + RoleSelectTest 8a/8b (glow + check state)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-03-30 18:31:05 -04:00
parent 8b006be138
commit a8592aeaec
11 changed files with 370 additions and 35 deletions

View File

@@ -45,6 +45,10 @@ var RoleSelect = (function () {
stack.dataset.starterRoles = current ? current + "," + roleCode : roleCode;
}
// Mark position as actively being seated (glow state)
var activePos = document.querySelector('.table-position[data-role-label="' + roleCode + '"]');
if (activePos) activePos.classList.add('active');
var url = getSelectRoleUrl();
if (!url) return;
fetch(url, {
@@ -72,6 +76,13 @@ var RoleSelect = (function () {
_animationPending = true;
Tray.placeCard(roleCode, function () {
_animationPending = false;
// Swap ban → check and clear glow on the seated position
var seatedPos = document.querySelector('.table-position[data-role-label="' + roleCode + '"]');
if (seatedPos) {
seatedPos.classList.remove('active');
var ban = seatedPos.querySelector('.fa-ban');
if (ban) { ban.classList.remove('fa-ban'); ban.classList.add('fa-circle-check'); }
}
if (_pendingTurnChange) {
var ev = _pendingTurnChange;
_pendingTurnChange = null;
@@ -178,6 +189,11 @@ var RoleSelect = (function () {
_turnChangedBeforeFetch = true;
if (typeof Tray !== "undefined") Tray.forceClose();
// Clear any stale .active glow from position indicators
document.querySelectorAll('.table-position.active').forEach(function (p) {
p.classList.remove('active');
});
var stack = document.querySelector(".card-stack[data-user-slots]");
if (stack) {
// Sync starter-roles from server so the fan reflects actual DB state

View File

@@ -367,67 +367,68 @@ class RoleSelectRenderingTest(TestCase):
self.room.save()
for i, gamer in enumerate(self.gamers, start=1):
TableSeat.objects.create(room=self.room, gamer=gamer, slot_number=i)
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
def test_room_view_includes_card_stack_when_role_select(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, "card-stack")
def test_card_stack_eligible_for_slot1_gamer(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, 'data-state="eligible"')
def test_card_stack_ineligible_for_slot2_gamer(self):
self.client.force_login(self.gamers[1])
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, 'data-state="ineligible"')
def test_card_stack_ineligible_shows_fa_ban(self):
self.client.force_login(self.gamers[1])
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, "fa-ban")
def test_card_stack_eligible_omits_fa_ban(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertNotContains(response, "fa-ban")
def test_gatekeeper_overlay_absent_when_role_select(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertNotContains(response, "gate-overlay")
def test_six_table_seats_rendered(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, "table-seat", count=6)
def test_active_table_seat_has_active_class(self):
self.client.force_login(self.founder) # slot 1 is active
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, 'class="table-seat active"')
def test_inactive_table_seat_lacks_active_class(self):
self.client.force_login(self.founder)
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
# Slots 26 are not active, so at least one plain table-seat exists
self.assertContains(response, 'class="table-seat"')
@@ -435,14 +436,14 @@ class RoleSelectRenderingTest(TestCase):
def test_card_stack_has_data_user_slots_for_eligible_gamer(self):
self.client.force_login(self.founder) # founder is slot 1 only
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, 'data-user-slots="1"')
def test_card_stack_has_data_user_slots_for_ineligible_gamer(self):
self.client.force_login(self.gamers[1]) # slot 2 gamer
response = self.client.get(
reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url
)
self.assertContains(response, 'data-user-slots="2"')
@@ -495,7 +496,7 @@ class PickRolesViewTest(TestCase):
reverse("epic:pick_roles", kwargs={"room_id": self.room.id})
)
self.assertRedirects(
response, reverse("epic:gatekeeper", args=[self.room.id])
response, reverse("epic:room", args=[self.room.id])
)
def test_pick_roles_notifies_channel_layer(self):
@@ -633,7 +634,7 @@ class SelectRoleViewTest(TestCase):
data={"role": "BOGUS"},
)
self.assertRedirects(
response, reverse("epic:gatekeeper", args=[self.room.id])
response, reverse("epic:room", args=[self.room.id])
)
def test_same_gamer_cannot_double_pick_sequentially(self):
@@ -648,7 +649,7 @@ class SelectRoleViewTest(TestCase):
data={"role": "BC"},
)
self.assertRedirects(
response, reverse("epic:gatekeeper", args=[self.room.id])
response, reverse("epic:room", args=[self.room.id])
)
self.assertEqual(
TableSeat.objects.filter(room=self.room, role__isnull=False).count(), 1
@@ -778,7 +779,7 @@ class SigSelectRenderingTest(TestCase):
def setUp(self):
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
self.url = reverse("epic:gatekeeper", kwargs={"room_id": self.room.id})
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
def test_sig_deck_element_present(self):
response = self.client.get(self.url)
@@ -878,7 +879,7 @@ class SelectSigCardViewTest(TestCase):
self.room.save()
response = self._post()
self.assertRedirects(
response, reverse("epic:gatekeeper", args=[self.room.id])
response, reverse("epic:room", args=[self.room.id])
)
def test_select_sig_last_choice_does_not_advance_to_none(self):

View File

@@ -6,6 +6,7 @@ app_name = 'epic'
urlpatterns = [
path('rooms/create_room', views.create_room, name='create_room'),
path('room/<uuid:room_id>/', views.room_view, name='room'),
path('room/<uuid:room_id>/gate/', views.gatekeeper, name='gatekeeper'),
path('room/<uuid:room_id>/gate/drop_token', views.drop_token, name='drop_token'),
path('room/<uuid:room_id>/gate/confirm_token', views.confirm_token, name='confirm_token'),

View File

@@ -71,6 +71,17 @@ def _notify_sig_selected(room_id, card_id, role, deck_type='levity'):
)
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
def _gate_positions(room):
"""Return list of dicts [{slot, role_label}] for _table_positions.html."""
return [
{"slot": slot, "role_label": SLOT_ROLE_LABELS.get(slot.slot_number, "")}
for slot in room.gate_slots.order_by("slot_number")
]
def _expire_reserved_slots(room):
cutoff = timezone.now() - RESERVE_TIMEOUT
room.gate_slots.filter(
@@ -135,6 +146,7 @@ def _gate_context(room, user):
"carte_slots_claimed": carte_slots_claimed,
"carte_nvm_slot_number": carte_nvm_slot_number,
"carte_next_slot_number": carte_next_slot_number,
"gate_positions": _gate_positions(room),
}
@@ -186,6 +198,7 @@ def _role_select_context(room, user):
.values_list("slot_number", flat=True)
) if user.is_authenticated else [],
"active_slot": active_slot,
"gate_positions": _gate_positions(room),
}
if room.table_status == Room.SIG_SELECT:
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
@@ -215,9 +228,16 @@ def create_room(request):
def gatekeeper(request, room_id):
room = Room.objects.get(id=room_id)
if room.table_status:
ctx = _role_select_context(room, request.user)
else:
ctx = _gate_context(room, request.user)
return redirect("epic:room", room_id=room_id)
ctx = _gate_context(room, request.user)
ctx["room"] = room
ctx["page_class"] = "page-gameboard"
return render(request, "apps/gameboard/room.html", ctx)
def room_view(request, room_id):
room = Room.objects.get(id=room_id)
ctx = _role_select_context(room, request.user)
ctx["room"] = room
ctx["page_class"] = "page-gameboard"
return render(request, "apps/gameboard/room.html", ctx)
@@ -390,17 +410,20 @@ def select_role(request, room_id):
if request.method == "POST":
room = Room.objects.get(id=room_id)
if room.table_status != Room.ROLE_SELECT:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect(
"epic:room" if room.table_status else "epic:gatekeeper",
room_id=room_id,
)
role = request.POST.get("role")
valid_roles = [code for code, _ in TableSeat.ROLE_CHOICES]
if not role or role not in valid_roles:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
with transaction.atomic():
active_seat = room.table_seats.select_for_update().filter(
role__isnull=True
).order_by("slot_number").first()
if not active_seat or active_seat.gamer != request.user:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
if room.table_seats.filter(role=role).exists():
return HttpResponse(status=409)
active_seat.role = role
@@ -416,7 +439,7 @@ def select_role(request, room_id):
record(room, GameEvent.ROLES_REVEALED)
_notify_roles_revealed(room_id)
return HttpResponse(status=200)
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
@login_required
@@ -433,7 +456,7 @@ def pick_roles(request, room_id):
slot_number=slot.slot_number,
)
_notify_role_select_start(room_id)
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
@login_required
@@ -487,7 +510,10 @@ def select_sig(request, room_id):
return redirect("epic:gatekeeper", room_id=room_id)
room = Room.objects.get(id=room_id)
if room.table_status != Room.SIG_SELECT:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect(
"epic:room" if room.table_status else "epic:gatekeeper",
room_id=room_id,
)
active_seat = active_sig_seat(room)
if active_seat is None or active_seat.gamer != request.user:
return HttpResponse(status=403)

View File

@@ -8,6 +8,7 @@ from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, select_token
from apps.lyric.models import Token, User
from .test_room_role_select import _fill_room_via_orm
class GatekeeperTest(FunctionalTest):
@@ -585,3 +586,120 @@ class GameKitInsertTest(FunctionalTest):
)
self.assertTrue(Token.objects.filter(id=pass_token.id).exists())
self.assertEqual(self.browser.current_url, self.gate_url)
class PositionIndicatorsTest(FunctionalTest):
"""Hex-side position indicators — always rendered outside the gatekeeper modal."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
self.create_pre_authenticated_session("founder@test.io")
self.founder, _ = User.objects.get_or_create(email="founder@test.io")
self.room = Room.objects.create(name="Position Test Room", owner=self.founder)
self.gate_url = (
f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
)
# ------------------------------------------------------------------ #
# Test P1 — 6 position indicators present while gatekeeper is open #
# ------------------------------------------------------------------ #
def test_position_indicators_visible_alongside_gatekeeper(self):
self.browser.get(self.gate_url)
# Gatekeeper modal is open
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
# Six .table-position elements are rendered outside the modal
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
self.assertEqual(len(positions), 6)
for pos in positions:
self.assertTrue(pos.is_displayed())
# ------------------------------------------------------------------ #
# Test P2 — URL drops /gate/ after pick_roles #
# ------------------------------------------------------------------ #
def test_url_drops_gate_after_pick_roles(self):
_fill_room_via_orm(self.room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
# Simulate pick_roles having fired: room advances to ROLE_SELECT
self.room.table_status = Room.ROLE_SELECT
self.room.save()
# Navigating to the /gate/ URL should redirect to the plain room URL
self.browser.get(self.gate_url)
expected_url = (
f"{self.live_server_url}/gameboard/room/{self.room.id}/"
)
self.wait_for(
lambda: self.assertEqual(self.browser.current_url, expected_url)
)
# ------------------------------------------------------------------ #
# Test P3 — Each position has a chair icon and correct role label #
# ------------------------------------------------------------------ #
def test_position_shows_chair_icon_and_role_label(self):
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
for slot_number, role_label in SLOT_ROLE_LABELS.items():
pos = self.browser.find_element(
By.CSS_SELECTOR, f".table-position[data-slot='{slot_number}']"
)
# Chair icon present
self.assertTrue(pos.find_elements(By.CSS_SELECTOR, ".fa-chair"))
# Role label attribute and visible text
self.assertEqual(pos.get_attribute("data-role-label"), role_label)
label_el = pos.find_element(By.CSS_SELECTOR, ".position-role-label")
self.assertEqual(label_el.text.strip(), role_label)
# ------------------------------------------------------------------ #
# Test P4 — Unoccupied position shows ban icon #
# ------------------------------------------------------------------ #
def test_unoccupied_position_shows_ban_icon(self):
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
# All slots are empty — every position should have a ban icon
positions = self.browser.find_elements(By.CSS_SELECTOR, ".table-position")
for pos in positions:
self.assertTrue(
pos.find_elements(By.CSS_SELECTOR, ".fa-ban"),
f"Expected .fa-ban on slot {pos.get_attribute('data-slot')}",
)
# ------------------------------------------------------------------ #
# Test P5 — Occupied position shows check icon after token confirmed #
# ------------------------------------------------------------------ #
def test_occupied_position_shows_check_icon_after_token_confirmed(self):
# Slot 1 is filled via ORM
from apps.epic.models import GateSlot
slot = self.room.gate_slots.get(slot_number=1)
slot.gamer = self.founder
slot.status = GateSlot.FILLED
slot.save()
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
pos1 = self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-slot='1']"
)
self.assertTrue(pos1.find_elements(By.CSS_SELECTOR, ".fa-circle-check"))
self.assertFalse(pos1.find_elements(By.CSS_SELECTOR, ".fa-ban"))

View File

@@ -484,6 +484,104 @@ class RoleSelectTest(FunctionalTest):
)
# ------------------------------------------------------------------ #
# Test 8a — Position glows while role card is being placed #
# ------------------------------------------------------------------ #
def test_position_glows_when_role_card_confirmed(self):
"""Immediately after confirming a role pick, the matching
.table-position should receive .active (the glow state) before
the tray animation completes."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Position Glow Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=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)
# Open fan, click first card (PC), confirm guard
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).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()
# PC position gains .active immediately after confirmation
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'].active"
)
)
# ------------------------------------------------------------------ #
# Test 8b — Position shows check icon after tray sequence ends #
# ------------------------------------------------------------------ #
def test_position_gets_check_when_tray_sequence_ends(self):
"""After the tray arc-in animation completes and the tray closes,
the PC .table-position should show .fa-circle-check and no .fa-ban."""
founder, _ = User.objects.get_or_create(email="founder@test.io")
room = Room.objects.create(name="Position Check Test", owner=founder)
_fill_room_via_orm(room, [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
])
room.table_status = Room.ROLE_SELECT
room.save()
for slot in room.gate_slots.order_by("slot_number"):
TableSeat.objects.create(
room=room, gamer=slot.gamer, slot_number=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)
# Open fan, pick PC card, confirm guard
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
)
).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()
# Wait for tray animation to complete (tray closes)
self.wait_for(
lambda: self.assertFalse(
self.browser.execute_script("return Tray.isOpen()"),
"Tray should close after arc-in sequence"
)
)
# PC position now shows check icon, ban icon gone
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-circle-check"
)
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".table-position[data-role-label='PC'] .fa-ban"
)),
0,
)
class RoleSelectTrayTest(FunctionalTest):
"""After confirming a role pick, the role card enters the tray grid and
the tray opens to reveal it.

View File

@@ -40,7 +40,7 @@
margin: 0;
padding: 0;
border: none;
border-top: 0.1rem solid rgba(var(--terUser), 0.3);
border-top: 0.1rem solid rgba(var(--quaUser), 1);
background: rgba(var(--priUser), 0.97);
z-index: 316;
overflow: hidden;
@@ -81,7 +81,7 @@
text-transform: uppercase;
text-decoration: underline;
letter-spacing: 0.12em;
color: rgba(var(--secUser), 0.35);
color: rgba(var(--quaUser), 0.75);
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
@@ -117,8 +117,8 @@
.kit-bag-placeholder {
font-size: 1.5rem;
opacity: 0.3;
padding: 0 0.125rem;
color: rgba(var(--quaUser), 0.3);
}
}

View File

@@ -36,25 +36,29 @@ $gate-line: 2px;
// disrupt pointer events on position:fixed descendants.
// NOTE: may be superfluous — root cause of CI kit-btn failures turned out to be
// game-kit.js missing from git (was in gitignored STATIC_ROOT only).
html:has(.gate-overlay) {
html:has(.gate-backdrop) {
overflow: hidden;
}
.gate-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 100;
pointer-events: none;
}
.gate-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 100;
z-index: 120;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
// Prevents backdrop from intercepting clicks on position:fixed elements
// (e.g. #id_kit_btn) in Linux headless Firefox.
// NOTE: may be superfluous — see html:has comment above.
pointer-events: none;
}
@@ -362,6 +366,63 @@ $seat-r: 130px;
$seat-r-x: round($seat-r * 0.866); // 113px
$seat-r-y: round($seat-r * 0.5); // 65px
// .table-position anchors at edge midpoints (pointy-top hex).
// Apothem ≈ 80px + 30px clearance = 110px total push from centre.
$pos-d: 110px;
$pos-d-x: round($pos-d * 0.5); // 55px
$pos-d-y: round($pos-d * 0.866); // 95px
.table-position {
position: absolute;
z-index: 110;
pointer-events: none;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
// Edge midpoints, clockwise from 3 o'clock (slot drop order → role order)
&[data-slot="1"] { left: calc(50% + #{$pos-d}); top: 50%; }
&[data-slot="2"] { left: calc(50% + #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
&[data-slot="3"] { left: calc(50% - #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
&[data-slot="4"] { left: calc(50% - #{$pos-d}); top: 50%; }
&[data-slot="5"] { left: calc(50% - #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
&[data-slot="6"] { left: calc(50% + #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
.position-body {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
}
.fa-chair {
font-size: 1.1rem;
color: rgba(var(--secUser), 0.4);
}
.position-role-label {
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(var(--secUser), 0.5);
}
.position-status-icon {
font-size: 0.65rem;
&.fa-ban { color: rgba(var(--priRd), 1); }
&.fa-circle-check { color: rgba(var(--priGn), 1); }
}
&.active {
.fa-chair {
color: rgba(var(--terUser), 1);
filter: drop-shadow(0 0 4px rgba(var(--ninUser), 1));
}
}
}
.room-table {
flex: 2;
position: relative;

View File

@@ -2,6 +2,7 @@
id="id_gate_wrapper"
data-gate-status-url="{% url 'epic:gate_status' room.id %}"
>
<div class="gate-backdrop"></div>
<div class="gate-overlay">
<div class="gate-modal" role="dialog" aria-label="Gatekeeper">

View File

@@ -0,0 +1,12 @@
{% for pos in gate_positions %}
<div class="table-position" data-slot="{{ pos.slot.slot_number }}" data-role-label="{{ pos.role_label }}">
<div class="position-body">
<i class="fa-solid fa-chair"></i>
<span class="position-role-label">{{ pos.role_label }}</span>
<div class="token-tooltip">
<h4>{% if pos.slot.gamer %}@{{ pos.slot.gamer.username|default:pos.slot.gamer.email }}{% else %}Empty Seat{% endif %}</h4>
</div>
</div>
<i class="position-status-icon fa-solid {% if pos.slot.gamer %}fa-circle-check{% else %}fa-ban{% endif %}"></i>
</div>
{% endfor %}

View File

@@ -46,6 +46,7 @@
</div>
{% endfor %}
{% endif %}
{% include "apps/gameboard/_partials/_table_positions.html" %}
</div>
</div>