table-hex: DRY-lift the shared hex skeleton into core/_partials/_table_hex.html

The .room-table-scene → .table-hex-border → .table-hex → .table-center + seats
ring was byte-identical across room.html, my_sea.html, my_sea_visit.html &
billboard/my_sign.html. Lift the skeleton into one cross-app partial so every
surface — and the upcoming Sea Select felt rebuild — shares a single source.

- core/_partials/_table_hex.html: the 4-div skeleton. The varying center +
  seats are passed as partial NAMES (`hex_center` / `hex_seats`) since Django
  {% include %} has no slot; `{% include hex_center %}` resolves the string var
  + inherits the full page context (no `only`), so each fragment sees its page's
  vars unchanged → identical render.
- Per-surface center fragments (verbatim moves): _room_hex_center,
  _my_sea_hex_center, _my_sea_visit_hex_center (gameboard/_partials),
  _my_sign_hex_center (billboard/_partials).
- Seats fragments: _room_hex_seats (gate_positions); _table_seats — SHARED by
  my_sea + my_sea_visit (`seats` loop; the `--self` modifier is inert on my_sea);
  _my_sign_hex_seats (single chair).
- The page-specific outer wrappers stay put (room's .room-shell + ROLE_SELECT
  SCAN SIGS form; .my-sea-landing / .my-sign-landing; my_sign's
  {% if not current_significator %} gate).

The "felt" is deliberately NOT extracted — it's a --duoUser bg toggled by
phase/stage classes (CSS), already DRY; each phase's felt content is bespoke.

Markup-only, no behaviour change. Verified: 1170 epic+gameboard+billboard render
ITs green (the table-hex / table-seat / center-btn assertions are the gate) +
MySeaDrawSeaLandingTest FTs green (live hex render + FREE DRAW seats
.table-seat[data-slot="1"] through the shared seats partial).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-07 19:37:50 -04:00
parent ce4cb03af7
commit 4322e1fc17
12 changed files with 190 additions and 178 deletions

View File

@@ -0,0 +1,7 @@
{% comment %}
my_sign.html table-hex center — included via core/_partials/_table_hex.html
(hex_center). The lone SCAN SIGN button (the my_sign hex renders only when the
user has no saved significator; that {% if not current_significator %} gate
stays in my_sign.html).
{% endcomment %}
<button id="id_scan_sign_btn" type="button" class="btn btn-primary">SCAN<br>SIGN</button>

View File

@@ -0,0 +1,8 @@
{% comment %}
my_sign.html table-hex seats — included via core/_partials/_table_hex.html
(hex_seats). Single founder chair — solo-coded but extensible to the 6-chair
friend-invite plan in [[project-my-sea-roadmap]].
{% endcomment %}
<div class="table-seat" data-slot="1" data-role="PC">
<i class="fa-solid fa-chair"></i>
</div>

View File

@@ -128,20 +128,7 @@ this billboard surface re-brands to "Sign".
{% if not current_significator %}
<div class="room-shell">
<div id="id_game_table" class="room-table">
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
<button id="id_scan_sign_btn" type="button" class="btn btn-primary">SCAN<br>SIGN</button>
</div>
</div>
</div>
{# Single founder chair — solo-coded but extensible to the #}
{# 6-chair friend-invite plan in [[project-my-sea-roadmap]]. #}
<div class="table-seat" data-slot="1" data-role="PC">
<i class="fa-solid fa-chair"></i>
</div>
</div>
{% include "core/_partials/_table_hex.html" with hex_center="apps/billboard/_partials/_my_sign_hex_center.html" hex_seats="apps/billboard/_partials/_my_sign_hex_seats.html" %}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,34 @@
{% comment %}
my_sea.html table-hex center — included via core/_partials/_table_hex.html
(hex_center). Verbatim move of the old inline .table-center body; inherits
my_sea.html's context (show_cont_draw, show_paid_draw, show_gate_view).
{% endcomment %}
{% if show_cont_draw %}
{# CONT DRAW — user came to the landing via the gear-menu NVM mid-draw #}
{# (?phase=landing). The picker hand is non-empty + incomplete; CONT DRAW #}
{# re-enters the picker (a fresh GET w.o. ?phase=landing falls through to #}
{# show_picker=True via the hand_non_empty branch). #}
<a id="id_my_sea_cont_draw_btn"
href="{% url 'my_sea' %}"
class="btn btn-primary">CONT<br>DRAW</a>
{% elif show_paid_draw %}
{# PAID DRAW — two underlying states collapse into one button (user-spec #}
{# 2026-05-23): #}
{# • `deposit_reserved` → POST commits the deposited token via #}
{# `my_sea_paid_draw` + redirects to picker. #}
{# • `paid_through` (token already spent this cycle, hand still empty) → #}
{# POST is a no-op commit branch in the view; redirects to picker anyway.#}
<form method="POST" action="{% url 'my_sea_paid_draw' %}" style="display:contents">
{% csrf_token %}
<button type="submit"
id="id_my_sea_paid_draw_btn"
class="btn btn-primary">PAID<br>DRAW</button>
</form>
{% elif show_gate_view %}
<button id="id_my_sea_gate_view_btn"
type="button"
class="btn btn-primary"
data-gate-url="{% url 'my_sea_gate' %}">GATE<br>VIEW</button>
{% else %}
<button id="id_draw_sea_btn" type="button" class="btn btn-primary">FREE<br>DRAW</button>
{% endif %}

View File

@@ -0,0 +1,16 @@
{% comment %}
my_sea_visit.html table-hex center — included via core/_partials/_table_hex.html
(hex_center). Verbatim move of the old inline .table-center body; inherits the
visit page context (seat2_present, owner).
{% endcomment %}
{% if seat2_present %}
{# Visitor is present — VIEW DRAW reveals the owner's read-only draw #}
{# (client-side toggle below). #}
<button id="id_my_sea_view_draw_btn" type="button"
class="btn btn-primary">VIEW<br>DRAW</button>
{% else %}
{# Not yet present — GATE VIEW → visitor token gate. #}
<button id="id_my_sea_gate_view_btn" type="button"
class="btn btn-primary"
data-gate-url="{% url 'my_sea_visit_gate' owner.id %}">GATE<br>VIEW</button>
{% endif %}

View File

@@ -0,0 +1,47 @@
{% comment %}
room.html table-hex center — included via core/_partials/_table_hex.html
(hex_center). Verbatim move of the old inline .table-center body; inherits
room.html's context (table_status, card_stack_state, viewer_cost_current,
sky_confirmed, polarity_done, user_polarity, current_slot, …).
{% endcomment %}
{# ROLE card-stack — the gamer's own role pick stays available even when their #}
{# token cost has lapsed (deposit-privilege grace, 7d), so it renders OUTSIDE #}
{# the cost gate below. #}
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}"
data-equipped-deck="{{ equipped_deck_id|default:'' }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% endif %}
{% if not viewer_cost_current %}
{# Token cost lapsed → GATE VIEW supersedes SCAN SIGS / CAST SKY / DRAW SEA #}
{# / the sig waiting msg. The gamer keeps their seat through the renewal #}
{# grace; GATE VIEW routes to the renewal gate-view. Only the ROLE pick #}
{# (above) survives. `<button>` + onclick (not `<a>`) — `.btn` doesn't reset #}
{# serif font on anchors. [[feedback-btn-vs-anchor-font-family]] #}
<button id="id_room_gate_view_btn" type="button"
class="btn btn-primary"
onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE<br>VIEW</button>
{% else %}
{% if room.table_status == "SKY_SELECT" %}
{# Both phase btns render so the post-save cascade can cross-fade CAST #}
{# SKY → DRAW SEA in place (no reload). The server sets --out on the #}
{# inactive one; sky-select.js's cascade swaps them. On a confirmed #}
{# reload the server lands DRAW SEA visible + CAST SKY out, same as the #}
{# cascade end. #}
<div class="hex-phase-stack">
<button id="id_pick_sky_btn" class="btn btn-primary hex-phase-btn{% if sky_confirmed %} hex-phase-btn--out{% endif %}">CAST<br>SKY</button>
<button id="id_pick_sea_btn" class="btn btn-primary hex-phase-btn{% if not sky_confirmed %} hex-phase-btn--out{% endif %}">DRAW<br>SEA</button>
</div>
{% elif room.table_status == "SIG_SELECT" %}
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
{% if polarity_done %}
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
{% endif %}
{% endif %}
{% endif %}

View File

@@ -0,0 +1,18 @@
{% comment %}
room.html table-hex seats — included via core/_partials/_table_hex.html
(hex_seats). Verbatim move of the gate_positions seat ring; inherits room.html's
context (gate_positions, starter_roles).
{% endcomment %}
{# Seats — fa-chair layout persists from ROLE_SELECT through SIG_SELECT #}
{% for pos in gate_positions %}
<div class="table-seat{% if pos.role_label in starter_roles %} role-confirmed{% endif %}"
data-slot="{{ pos.slot.slot_number }}" data-role="{{ pos.role_label }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-role-label">{{ pos.role_label }}</span>
{% if pos.role_label in starter_roles %}
<i class="position-status-icon fa-solid fa-circle-check"></i>
{% else %}
<i class="position-status-icon fa-solid fa-ban"></i>
{% endif %}
</div>
{% endfor %}

View File

@@ -0,0 +1,22 @@
{% comment %}
Shared table-hex seats ring for my_sea.html + my_sea_visit.html — included via
core/_partials/_table_hex.html (hex_seats). Iterates the `seats` context
(seat.present / .n / .label / .token). The `.table-seat--self` modifier marks
the visitor's own seat on my_sea_visit; it is inert on my_sea (seat.is_self is
undefined → falsy).
{% endcomment %}
{% for seat in seats %}
{% if seat.present %}
<div class="table-seat seated{% if seat.is_self %} table-seat--self{% endif %}" data-slot="{{ seat.n }}" data-seat-token="{{ seat.token }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-circle-check"></i>
</div>
{% else %}
<div class="table-seat" data-slot="{{ seat.n }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-ban"></i>
</div>
{% endif %}
{% endfor %}

View File

@@ -48,66 +48,11 @@
<div class="my-sea-landing">
<div class="room-shell">
<div id="id_game_table" class="room-table">
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{% if show_cont_draw %}
{# CONT DRAW — user came to the landing via the gear- #}
{# menu NVM mid-draw (?phase=landing). The picker hand #}
{# is non-empty + incomplete; CONT DRAW re-enters the #}
{# picker (a fresh GET w.o. ?phase=landing falls through #}
{# to show_picker=True via the hand_non_empty branch). #}
<a id="id_my_sea_cont_draw_btn"
href="{% url 'my_sea' %}"
class="btn btn-primary">CONT<br>DRAW</a>
{% elif show_paid_draw %}
{# PAID DRAW — two underlying states collapse into one #}
{# button (user-spec 2026-05-23): #}
{# • `deposit_reserved` → POST commits the deposited #}
{# token via `my_sea_paid_draw` + redirects to picker. #}
{# • `paid_through` (token already spent this cycle, #}
{# hand still empty) → POST is a no-op commit branch #}
{# in the view; the view redirects to picker anyway. #}
<form method="POST" action="{% url 'my_sea_paid_draw' %}" style="display:contents">
{% csrf_token %}
<button type="submit"
id="id_my_sea_paid_draw_btn"
class="btn btn-primary">PAID<br>DRAW</button>
</form>
{% elif show_gate_view %}
<button id="id_my_sea_gate_view_btn"
type="button"
class="btn btn-primary"
data-gate-url="{% url 'my_sea_gate' %}">GATE<br>VIEW</button>
{% else %}
<button id="id_draw_sea_btn" type="button" class="btn btn-primary">FREE<br>DRAW</button>
{% endif %}
</div>
</div>
</div>
{# Owner 1C + present visitors 2C-6C — the shared #}
{# `_my_sea_seats` ring, so the owner sees who's #}
{# seated at her table, not just herself. The FREE #}
{# DRAW client-side transition still targets #}
{# `.table-seat[data-slot="1"]`. `.position-status- #}
{# icon` / `.fa-ban` are role-agnostic in _room.scss. #}
{% for seat in seats %}
{% if seat.present %}
<div class="table-seat seated" data-slot="{{ seat.n }}" data-seat-token="{{ seat.token }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-circle-check"></i>
</div>
{% else %}
<div class="table-seat" data-slot="{{ seat.n }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-ban"></i>
</div>
{% endif %}
{% endfor %}
</div>
{# Owner 1C + present visitors 2C-6C — the shared seats ring #}
{# (_table_seats.html), so the owner sees who's seated at her #}
{# table, not just herself. The FREE DRAW client-side #}
{# transition still targets `.table-seat[data-slot="1"]`. #}
{% include "core/_partials/_table_hex.html" with hex_center="apps/gameboard/_partials/_my_sea_hex_center.html" hex_seats="apps/gameboard/_partials/_table_seats.html" %}
</div>
</div>
</div>

View File

@@ -20,44 +20,11 @@
<div class="my-sea-landing my-sea-visit-landing">
<div class="room-shell">
<div id="id_game_table" class="room-table">
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{% if seat2_present %}
{# Visitor is present — VIEW DRAW reveals the owner's #}
{# read-only draw (client-side toggle below). #}
<button id="id_my_sea_view_draw_btn" type="button"
class="btn btn-primary">VIEW<br>DRAW</button>
{% else %}
{# Not yet present — GATE VIEW → visitor token gate. #}
<button id="id_my_sea_gate_view_btn" type="button"
class="btn btn-primary"
data-gate-url="{% url 'my_sea_visit_gate' owner.id %}">GATE<br>VIEW</button>
{% endif %}
</div>
</div>
</div>
{# Every present member shows on the ring — owner 1C + #}
{# present invitees 2C6C by deposit order (the viewer's #}
{# own seat carries `--self`). Built server-side as #}
{# `seats` so all viewers see identical absolute seating. #}
{% for seat in seats %}
{% if seat.present %}
<div class="table-seat seated{% if seat.is_self %} table-seat--self{% endif %}" data-slot="{{ seat.n }}" data-seat-token="{{ seat.token }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-circle-check"></i>
</div>
{% else %}
<div class="table-seat" data-slot="{{ seat.n }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-position-label">{{ seat.label }}</span>
<i class="position-status-icon fa-solid fa-ban"></i>
</div>
{% endif %}
{% endfor %}
</div>
{# Every present member shows on the ring (the shared #}
{# _table_seats.html) — owner 1C + present invitees 2C6C by #}
{# deposit order; the viewer's own seat carries `--self`. Built #}
{# server-side as `seats` so all viewers see identical seating. #}
{% include "core/_partials/_table_hex.html" with hex_center="apps/gameboard/_partials/_my_sea_visit_hex_center.html" hex_seats="apps/gameboard/_partials/_table_seats.html" %}
</div>
</div>
</div>

View File

@@ -50,72 +50,7 @@
</form>
</div>
{% endif %}
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{# ROLE card-stack — the gamer's own role pick stays #}
{# available even when their token cost has lapsed #}
{# (deposit-privilege grace, 7d), so it renders OUTSIDE #}
{# the cost gate below. #}
{% if room.table_status == "ROLE_SELECT" and card_stack_state %}
<div class="card-stack" data-state="{{ card_stack_state }}"
data-starter-roles="{{ starter_roles|join:',' }}"
data-user-slots="{{ user_slots|join:',' }}"
data-active-slot="{{ active_slot }}"
data-equipped-deck="{{ equipped_deck_id|default:'' }}">
{% if card_stack_state == "ineligible" %}
<i class="fa-solid fa-ban"></i>
{% endif %}
</div>
{% endif %}
{% if not viewer_cost_current %}
{# Token cost lapsed → GATE VIEW supersedes SCAN SIGS #}
{# / CAST SKY / DRAW SEA / the sig waiting msg. The #}
{# gamer keeps their seat through the renewal grace; #}
{# GATE VIEW routes to the renewal gate-view. Only #}
{# the ROLE pick (above) survives. `<button>` + #}
{# onclick (not `<a>`) — `.btn` doesn't reset serif #}
{# font on anchors. [[feedback-btn-vs-anchor-font- #}
{# family]] #}
<button id="id_room_gate_view_btn" type="button"
class="btn btn-primary"
onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE<br>VIEW</button>
{% else %}
{% if room.table_status == "SKY_SELECT" %}
{# Both phase btns render so the post-save cascade can #}
{# cross-fade CAST SKY → DRAW SEA in place (no reload). The #}
{# server sets --out on the inactive one; sky-select.js's #}
{# cascade swaps them. On a confirmed reload the server lands #}
{# DRAW SEA visible + CAST SKY out, same as the cascade end. #}
<div class="hex-phase-stack">
<button id="id_pick_sky_btn" class="btn btn-primary hex-phase-btn{% if sky_confirmed %} hex-phase-btn--out{% endif %}">CAST<br>SKY</button>
<button id="id_pick_sea_btn" class="btn btn-primary hex-phase-btn{% if not sky_confirmed %} hex-phase-btn--out{% endif %}">DRAW<br>SEA</button>
</div>
{% elif room.table_status == "SIG_SELECT" %}
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
{% if polarity_done %}
<p id="id_hex_waiting_msg">{% if user_polarity == "levity" %}Gravity settling . . .{% else %}Levity appraising . . .{% endif %}</p>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
{# Seats — fa-chair layout persists from ROLE_SELECT through SIG_SELECT #}
{% for pos in gate_positions %}
<div class="table-seat{% if pos.role_label in starter_roles %} role-confirmed{% endif %}"
data-slot="{{ pos.slot.slot_number }}" data-role="{{ pos.role_label }}">
<i class="fa-solid fa-chair"></i>
<span class="seat-role-label">{{ pos.role_label }}</span>
{% if pos.role_label in starter_roles %}
<i class="position-status-icon fa-solid fa-circle-check"></i>
{% else %}
<i class="position-status-icon fa-solid fa-ban"></i>
{% endif %}
</div>
{% endfor %}
</div>
{% include "core/_partials/_table_hex.html" with hex_center="apps/gameboard/_partials/_room_hex_center.html" hex_seats="apps/gameboard/_partials/_room_hex_seats.html" %}
</div>
</div>{# /.room-shell #}
{# Sig Select stage — green-felt card stage + thumbnail grid, rendered #}

View File

@@ -0,0 +1,26 @@
{% comment %}
Shared hexagonal game table — the .room-table-scene skeleton (border / hex /
center) + the seats ring. This 4-div nesting is byte-identical across room.html,
my_sea.html, my_sea_visit.html and billboard/my_sign.html, so it lives here once.
The CENTER content and the SEATS loop vary per surface, and Django {% include %}
has no block/slot, so each caller passes them as partial NAMES:
{% include "core/_partials/_table_hex.html"
with hex_center="apps/gameboard/_partials/_room_hex_center.html"
hex_seats="apps/gameboard/_partials/_room_hex_seats.html" %}
`{% include hex_center %}` resolves the string var to a template and inherits the
full page context (no `only`), so each center/seats partial sees its page's vars
unchanged — the markup is identical to the pre-extraction inline blocks.
{% endcomment %}
<div class="room-table-scene">
<div class="table-hex-border">
<div class="table-hex">
<div class="table-center">
{% include hex_center %}
</div>
</div>
</div>
{% include hex_seats %}
</div>