Workstream A of the position-circle tooltips sprint (green; B/C ride @skip-ped).
The numbered gate-position circles (1-6) gain rich hover tooltips mirroring the My Buds bud tooltip on every surface — and now render on room_gate.html (the GATE VIEW), which showed no circles before (the headline gap).
- _gate_positions(room, user, current_slot): per-circle .tt-pos-* state class (empty / gamer / gamer+bud / me-current / me-also) + data-tt-* payload (@handle via at_handle NOT email, title, seat significator rank/suit, bud shoptalk, deposited #tokens [CARTE slots_claimed else 1], seat-clock cost_current_until expiry). _viewer_current_slot resolves the viewer's acting seat (?seat override or canonical) to split me-current vs me-also.
- room_gate view merges _gate_context so _table_positions renders there; room_view threads ?seat into _role_select_context.
- _table_positions.html: .tt-pos-* appended AFTER role-assigned (keeps the 'gate-slot filled role-assigned' substring + class-before-data-slot regex intact for RoleSelectRenderingTest), data-tt-* attrs, me-also ?seat switch anchor.
- #id_position_tooltip_portal (page-root, position:fixed) + position-tooltip.js (hover/clamp/union-hide modeled on tray-tooltip.js); .tt-sign rank+suit stack; .tt-pos-* circle accents; room-gate pointer-events re-enable.
Tests: 7 PositionTooltipTest + 2 CarteSeatSwitchTest (tokens, me-also href) FTs green; 8 fast render-level ITs (PositionTooltip{,Carte}RenderTest); full suite 1598 green.
[[project-position-circle-tooltips]]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
12 KiB
HTML
177 lines
12 KiB
HTML
{% extends "core/base.html" %}
|
|
{% load static tooltip_tags %}
|
|
|
|
{% block title_text %}Gameboard{% endblock title_text %}
|
|
{% block header_text %}<span>Game</span><span>room</span>{% endblock header_text %}
|
|
|
|
{% block content %}
|
|
<div class="room-page" data-room-id="{{ room.id }}"
|
|
{% if room.table_status %}data-select-role-url="{% url 'epic:select_role' room.id %}"{% endif %}>
|
|
<div id="id_aperture_fill"></div>
|
|
<div class="room-shell">
|
|
<div id="id_game_table" class="room-table">
|
|
{# SCAN SIGS advances the whole table past role-select — gated on #}
|
|
{# the viewer's token cost being current (a lapsed gamer gets GATE #}
|
|
{# VIEW in the center instead; only their own ROLE pick survives). #}
|
|
{% if room.table_status == "ROLE_SELECT" and viewer_cost_current %}
|
|
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
|
|
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
|
|
{% csrf_token %}
|
|
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">SCAN<br>SIGS</button>
|
|
</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 %}'">GATE<br>VIEW</button>
|
|
{% else %}
|
|
{% if room.table_status == "SKY_SELECT" %}
|
|
{% if sky_confirmed %}
|
|
<button id="id_pick_sea_btn" class="btn btn-primary">DRAW<br>SEA</button>
|
|
{% else %}
|
|
<button id="id_pick_sky_btn" class="btn btn-primary">CAST<br>SKY</button>
|
|
{% endif %}
|
|
{% 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>
|
|
</div>
|
|
</div>
|
|
|
|
{# Phase overlays are gated on `viewer_cost_current` too: a lapsed gamer #}
|
|
{# gets GATE VIEW in the center, so the SIG/SKY/SEA modals (which embed #}
|
|
{# their trigger-btn ids in JS) must not render alongside it. #}
|
|
{# Sig Select overlay — suppressed once this gamer's polarity sigs are assigned #}
|
|
{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %}
|
|
{% include "apps/gameboard/_partials/_sig_select_overlay.html" %}
|
|
{% endif %}
|
|
|
|
{# Sky (Pick Sky) overlay — natal chart entry #}
|
|
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
|
|
{% include "apps/gameboard/_partials/_sky_overlay.html" %}
|
|
{% endif %}
|
|
{# Sky tooltip: sibling of .sky-overlay, not inside .sky-modal-wrap (which has transform) #}
|
|
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
|
|
<div id="id_sky_tooltip" class="tt" style="display:none;"></div>
|
|
<div id="id_sky_tooltip_2" class="tt" style="display:none;"></div>
|
|
{% endif %}
|
|
|
|
{# Sea (Pick Sea) overlay — Celtic Cross spread entry #}
|
|
{% if room.table_status == "SKY_SELECT" and sky_confirmed and viewer_cost_current %}
|
|
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
|
|
{% endif %}
|
|
|
|
{# Gamer-needed stub — a seat lapsed past its renewal grace and was #}
|
|
{# auto-BYE'd, so the table no longer fills all six. Minimal stub #}
|
|
{# until the mid-game re-seat flow lands (_table_positions + #}
|
|
{# _gatekeeper are already suppressed for RENEWAL_DUE below). #}
|
|
{% if room.gate_status == "RENEWAL_DUE" %}
|
|
<div id="id_gamer_needed" class="hex-stub">
|
|
<p>A seat opened — awaiting a gamer.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if room.gate_status != "RENEWAL_DUE" and room.table_status != "SIG_SELECT" %}
|
|
{% include "apps/gameboard/_partials/_table_positions.html" %}
|
|
{% endif %}
|
|
{% if not room.table_status and room.gate_status != "RENEWAL_DUE" %}
|
|
{% include "apps/gameboard/_partials/_gatekeeper.html" %}
|
|
{# Owner-only invite affordance: handshake btn at the upper-right #}
|
|
{# of the right sidebar w. slide-out + autocomplete. Replaces the #}
|
|
{# legacy inline `<form action="invite_gamer">` panel. #}
|
|
{% if request.user == room.owner %}
|
|
{% include "apps/billboard/_partials/_bud_invite_panel.html" %}
|
|
{% endif %}
|
|
{% endif %}
|
|
{% if room.table_status %}
|
|
<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">
|
|
<i class="fa-solid fa-dice-d20"></i>
|
|
</button>
|
|
</div>
|
|
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}">{% tooltip my_tray_role_tooltip %}</div>{% else %}<div class="tray-cell"></div>{% endif %}{% if my_tray_sig %}<div class="tray-cell tray-sig-card"><div class="sig-stage-card sea-sig-card" aria-label="{{ my_tray_sig.name }}" data-energies="{{ my_tray_sig.energies_json }}" data-operations="{{ my_tray_sig.operations_json }}"><span class="fan-corner-rank">{{ my_tray_sig.corner_rank }}</span>{% if my_tray_sig.suit_icon %}<i class="fa-solid {{ my_tray_sig.suit_icon }}"></i>{% endif %}</div></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
|
|
</div>
|
|
{% endif %}
|
|
{% comment %}
|
|
Tray-tooltip portal — sibling of the tray so it sits at room-page root,
|
|
not inside the tray's overflow:hidden / mask-image clip. JS populates
|
|
innerHTML on hover of .tray-role-card > img (and Phase 2: sig card).
|
|
{% endcomment %}
|
|
{% if room.table_status %}
|
|
<div id="id_tooltip_portal" class="tt" style="display:none;"></div>
|
|
{% endif %}
|
|
{# Position-circle tooltip portal — rendered whenever the circles can #}
|
|
{# (gatekeeper + role-select; the SIG_SELECT phase hides the strip). #}
|
|
{% include "apps/gameboard/_partials/_position_tooltip.html" %}
|
|
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
|
{% include "apps/gameboard/_partials/_burger.html" %}
|
|
</div>
|
|
{% endblock content %}
|
|
|
|
{% block scripts %}
|
|
{# Brief module — needed by _bud_invite_panel's OK handler so the #}
|
|
{# slide-down banner shows up on a successful gatekeeper invite. #}
|
|
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
|
<script src="{% static 'apps/epic/room.js' %}"></script>
|
|
<script src="{% static 'apps/epic/gatekeeper.js' %}"></script>
|
|
<script src="{% static 'apps/epic/role-select.js' %}"></script>
|
|
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
|
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
|
<script src="{% static 'apps/epic/sig-select.js' %}"></script>
|
|
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
|
<script src="{% static 'apps/epic/tray.js' %}"></script>
|
|
<script src="{% static 'apps/epic/tray-tooltip.js' %}"></script>
|
|
<script src="{% static 'apps/epic/position-tooltip.js' %}"></script>
|
|
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
|
|
{% endblock scripts %}
|