Files
python-tdd/src/templates/apps/gameboard/room.html

177 lines
12 KiB
HTML
Raw Normal View History

{% extends "core/base.html" %}
tray cards: shadow, hover-tilt w. focus persistence, role-card tooltip — TDD - _tray.scss: drop-shadow on cell child elements (img → filter:drop-shadow so the silhouette is the shadow caster, div → box-shadow); 7° hover-tilt on .tray-role-card > img (-7°) and .tray-sig-card > .sig-stage-card (+7° via the standalone `rotate` property so the existing -5° baseline transform composes); :focus persists the tilt after click; cursor: pointer - tray.js: set tabIndex=0 on placeCard's role cell + on template-rendered .tray-role-card / .tray-sig-card cells at init() so :focus latches the hover state; clear tabindex in reset() for Jasmine afterEach - TraySpec: 4 new specs covering placeCard tabindex, reset cleanup, init-time tabindex on template-rendered sig & role cards, no-tabindex on bare cells - New tray-tooltip.js (#id_tooltip_portal) — Phase 1 of the apps.tooltips integration: hovering .tray-role-card > img copies its sibling .tt's innerHTML into the page-root portal, anchors above/below the trigger, & clamps to the viewport horizontally; mousemove outside the union of [trigger, portal] rects clears the portal (Game-Kit pattern, no btns) - room.html: #id_tooltip_portal mounted at room-page root (outside tray's overflow:hidden); .tt block rendered inline inside .tray-role-card via {% tooltip %} templatetag w. title=role display name & description="[Placeholder description]" - epic/views.py: my_tray_role_tooltip context dict ({title, description}) keyed off the seated role - TrayTooltipSpec: 8 specs covering portal population, .active class, sibling-.tt fallback, viewport-edge clamp left/right, and union-rect mouseleave - 2 FTs in test_component_tray_tooltip.py: hover role img → portal title=Player + description=Placeholder; mouseleave → portal clears Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel via a DRY refactor) deferred per plan. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:40:10 -04:00
{% load static tooltip_tags %}
{% block title_text %}Gameboard{% endblock title_text %}
fluid root rem + landscape aperture: html font-size = clamp(14px, 2.4vmin, 22px) so 1rem scales w. viewport (rotation-invariant via vmin); --sidebar-w + --h2-col-w CSS vars unify navbar/footer/h2 sizing; container margin-left = sidebar + h2-col-w in landscape so applets clip cleanly under the rotated wordmark; h2 markup splits into two spans (45/55 horizontal title); drop the disparate min-height font-size jumps + 1800px sidebar-doubling overrides - html { font-size: clamp(14px, 2.4vmin, 22px) } — single sliding scale; everything in rem (sidebar widths, h2 font-size, paddings) scales together. Phone rotation swaps width/height but vmin stays the same → 1rem stays the same → navbar/footer/h2 hold their size between portrait + landscape. - :root --sidebar-w: 5rem (replaces the locally-scoped $sidebar-w SCSS var that lived inside @media blocks); --h2-col-w: 3rem for the rotated wordmark column in landscape. var(--sidebar-w) + var(--h2-col-w) are the only knobs that move the layout. - Landscape container: margin-left = calc(var(--sidebar-w) + var(--h2-col-w)); margin-right = var(--sidebar-w). Applets are now clipped INSIDE the h2 column, so the rotated "BILLPOST" / "DASHBOARD" wordmark never has content bleeding behind it (the original complaint). - h2 markup refactor across 13 templates: <span>BILL</span><span>POST</span> instead of <span>BILL</span>POST. Portrait styling: display: flex; first span flex 0 0 45% + --quaUser colour; second span flex 0 0 55% + --secUser inherited. Per-span text-align: justify + text-justify: inter-character keeps the inter-letter spacing within each span. Landscape resets the flex (single rotated wordmark, not split). - Drop the four h2 font-size jumps (min-height: 400/500/800px) — single font-size: 3rem now scales fluidly via root rem. Drop the @media (orientation: landscape) and (max-width: 1100px) h1 override (rem-fluid handles cramped widths). Drop the entire @media (orientation: landscape) and (min-width: 1800px) sidebar-doubling block in _base.scss / _applets.scss / _bud.scss — the rem clamp ceiling already caps the size. - _bud.scss + _applets.scss: bud-btn / bud-panel / bud-suggestions / gear-btn / applet menus all switch to var(--sidebar-w)-based positioning; landscape rules are single (no per-breakpoint duplication). - Per-spec tradeoff: non-.btn-primary buttons (BYE / NVM / OK / kit-btn / etc.) inherit rem-fluid like everything else and will scale slightly w. viewport. User explicitly OK'd this — they don't need to stay px-fixed. - 852 ITs + 24 layout/navbar/bud FTs green; existing geometry assertions are relative or categorical (not exact-px) so the rem clamp doesn't surface failures at the 800x1200 FT viewport. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:14:14 -04:00
{% 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">
2026-05-31 23:29:43 -04:00
{# 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 %}
btn-primary label renames + stage-card polarity color refinements — two interleaved threads from one session, committing together since both touch sig + sea stage cards ; LABEL RENAMES: PICK SIGS → SCAN SIGS (room.html #id_pick_sigs_btn), PICK SKY → CAST SKY (room.html #id_pick_sky_btn × 2), PICK SEA → DRAW SEA (room.html #id_pick_sea_btn), TAKE SIG → SAVE SIG (sig-select.js _takeSigBtn.textContent × 2 callsites + section comment) — Element IDs (id_pick_sky_btn etc.), URL names (epic:pick_sigs, epic:pick_sky), and Python state enums (TableStatus.PICK_SKY, PICK_SEA, SIG_SELECT) intentionally retained as stable identifiers; the renamed text is purely the .btn-primary user-facing label ; FT + IT mentions of the old labels swept in test_game_room_select_{sig,sky,sea,role}.py, test_billboard.py, setup_sea_session.py mgmt cmd, apps/epic/{views,utils,models,tasks,tests/integrated/test_views}.py, SigSelectSpec.js, sky_overlay/sea_overlay/dashboard/sky.html, _card-deck.scss, _sky.scss — all docstring/comment references updated for cascade-grep cleanliness ; STAGE-CARD COLOR + CLASS REFINEMENTS (earlier in session): sig-stage card text colour split per polarity — gravity gets --terUser on .fan-card-name + .fan-card-reversal-{name,qualifier} + .sig-qualifier-{above,below}, levity gets --quiUser on the same five slots; all selectors prefixed w. .sig-stage-card to match the 0,4,0 specificity of the default `.sig-stage .sig-stage-card .fan-card-face .sig-qualifier-*` rule (without the prefix the polarity overrides lose the cascade — .sig-qualifier-below was visibly stuck on the default --quiUser) ; .stat-face-label gets polarity-inverse colours — gravity stat-block bg is --secUser (opposite of card's --priUser) so the label takes --quiUser to stay legible; levity is the symmetric flip (label = --terUser on --priUser stat-block bg) ; levity card title/qualifier drop-shadow swapped from rgba(0,0,0,…) → rgba(255,255,255,…) — dark drop reads as harsh smudge against the inverted-frame levity --secUser bg; applied to both sig-overlay[data-polarity="levity"] stage card AND sea-stage--levity via $_sea-title-shadow-levity (former shared $_sea-title-shadow split into per-polarity {levity,gravity} variants) ; reversal-face class/content alignment so each `.fan-card-reversal-*` class always carries its semantic content — DOM order per arcana type controls visual layout after the 180° SPIN (DOM-second appears visually on top): Major → title in .fan-card-reversal-name @ DOM-second (visually top after spin), qualifier in .fan-card-reversal-qualifier @ DOM-first; Non-major → title in .fan-card-reversal-name @ DOM-first (visually bottom after spin), qualifier in .fan-card-reversal-qualifier @ DOM-second (preserves the original "qualifier word reads first after spin" layout for Middle/Minor arcana — e.g. "Relieving / Eight of Crowns" not "Eight of Crowns / Relieving") ; _tarot_fan.html renders per-arcana DOM order directly (Django template branches handle both layouts); sig + sea overlays render a fixed two-`<p>` skeleton (one DOM order) so stage-card.js's populator dynamically rewrites the two `<p>`s' className per arcana — Major/override branch flips DOM-second to .fan-card-reversal-name + content, DOM-first to .fan-card-reversal-qualifier; non-major branch keeps DOM-first as .fan-card-reversal-name + title, DOM-second as .fan-card-reversal-qualifier + reversalQualifier-or-polarity-fallback ; SigSelectSpec.js + SeaDealSpec.js fixtures + Major reversed-face assertion updated for the new semantic — TDD Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:25:10 -04:00
<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">
2026-05-31 23:29:43 -04:00
{# 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 %}
2026-05-31 23:29:43 -04:00
{% 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>
2026-05-31 23:29:43 -04:00
{# 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 #}
2026-05-31 23:29:43 -04:00
{% 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 #}
2026-05-31 23:29:43 -04:00
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sky_overlay.html" %}
PICK SKY overlay: D3 natal wheel, Character model, PySwiss aspects+tz PySwiss: - calculate_aspects() in calc.py (conjunction/sextile/square/trine/opposition with orbs) - /api/tz/ endpoint (timezonefinder lat/lon → IANA timezone) - aspects included in /api/chart/ response - timezonefinder==8.2.2 added to requirements - 14 new unit tests (test_calc.py) + 12 new integration tests (TimezoneApiTest, aspect fields) Main app: - Sign, Planet, AspectType, HouseLabel reference models + seeded migrations (0032–0033) - Character model with birth_dt/lat/lon/place, house_system, chart_data, celtic_cross, confirmed_at/retired_at lifecycle (migration 0034) - natus_preview proxy view: calls PySwiss /api/chart/ + optional /api/tz/ auto-resolution, computes planet-in-house distinctions, returns enriched JSON - natus_save view: find-or-create draft Character, confirmed_at on action='confirm' - natus-wheel.js: D3 v7 SVG natal wheel (elements pie, signs, houses, planets, aspects, ASC/MC axes); NatusWheel.draw() / redraw() / clear() - _natus_overlay.html: Nominatim place autocomplete (debounced 400ms), geolocation button with reverse-geocode city name, live chart preview (debounced 300ms), tz auto-fill, NVM / SAVE SKY footer; html.natus-open class toggle pattern - _natus.scss: Gaussian backdrop+modal, two-column form|wheel layout, suggestion dropdown, portrait collapse at 600px, landscape sidebar z-index sink - room.html: include overlay when table_status == SKY_SELECT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 02:09:26 -04:00
{% endif %}
{# Sky tooltip: sibling of .sky-overlay, not inside .sky-modal-wrap (which has transform) #}
2026-05-31 23:29:43 -04:00
{% 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 %}
PICK SKY overlay: D3 natal wheel, Character model, PySwiss aspects+tz PySwiss: - calculate_aspects() in calc.py (conjunction/sextile/square/trine/opposition with orbs) - /api/tz/ endpoint (timezonefinder lat/lon → IANA timezone) - aspects included in /api/chart/ response - timezonefinder==8.2.2 added to requirements - 14 new unit tests (test_calc.py) + 12 new integration tests (TimezoneApiTest, aspect fields) Main app: - Sign, Planet, AspectType, HouseLabel reference models + seeded migrations (0032–0033) - Character model with birth_dt/lat/lon/place, house_system, chart_data, celtic_cross, confirmed_at/retired_at lifecycle (migration 0034) - natus_preview proxy view: calls PySwiss /api/chart/ + optional /api/tz/ auto-resolution, computes planet-in-house distinctions, returns enriched JSON - natus_save view: find-or-create draft Character, confirmed_at on action='confirm' - natus-wheel.js: D3 v7 SVG natal wheel (elements pie, signs, houses, planets, aspects, ASC/MC axes); NatusWheel.draw() / redraw() / clear() - _natus_overlay.html: Nominatim place autocomplete (debounced 400ms), geolocation button with reverse-geocode city name, live chart preview (debounced 300ms), tz auto-fill, NVM / SAVE SKY footer; html.natus-open class toggle pattern - _natus.scss: Gaussian backdrop+modal, two-column form|wheel layout, suggestion dropdown, portrait collapse at 600px, landscape sidebar z-index sink - room.html: include overlay when table_status == SKY_SELECT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 02:09:26 -04:00
{# Sea (Pick Sea) overlay — Celtic Cross spread entry #}
2026-05-31 23:29:43 -04:00
{% if room.table_status == "SKY_SELECT" and sky_confirmed and viewer_cost_current %}
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
{% endif %}
room auto-BYE: free seats lapsed past the renewal grace (2× renewal_period) → RENEWAL_DUE gamer-needed stub — TDD Phase 5 of the room GATE VIEW + seat-renewal sprint. A seated gamer who never renews is evicted once their seat's cost passes the renewal-grace window (filled_at + 2*renewal_period; 14d at the 7d default). - _expire_lapsed_seats(room): mirrors _expire_reserved_slots — for each FILLED slot past 2S, blanks the GateSlot, blanks the matching TableSeat (keeps the row for seat-count integrity), records SLOT_RETURNED + retracts the prior SLOT_FILLED (scroll redact-pair symmetry), then flags the room RENEWAL_DUE. NULL filled_at is never expired (RESERVED holds / ORM fixtures / auto-admit trinkets) — protects every existing FILLED-slot test - lazy call sites: room_view, gatekeeper, room_gate (on access; mirrors the my-sea delete_stale pattern — no scheduler needed for active rooms) - room.html: RENEWAL_DUE renders a minimal #id_gamer_needed stub (_table_positions + _gatekeeper already suppressed for RENEWAL_DUE). Mid-game re-seat flow is a documented follow-on Tests: ExpireLapsedSeatsTest (10) — frees slot + blanks seat past grace; no-op within cost window / grace / for null filled_at; sets RENEWAL_DUE; records SLOT_RETURNED; lazy expiry on room_view + room_gate access; gamer-needed stub renders. 848 epic+gameboard ITs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:34:20 -04:00
{# 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" %}
gatekeeper invite ports to #id_bud_btn slide-out: drop the inline #id_invite_email form, add bud-invite panel for room owner during gate phase, async POST to invite_gamer w. autocomplete + symmetric buds auto-add + slide-down Brief banner — TDD - Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set. - epic.invite_gamer view refactor: • Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat). • Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username). • RoomInvite stores the resolved User's email (or raw input if unregistered). • Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User. • Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null. • Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before. • Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch. - _gatekeeper.html: gate-invite-panel block (lines 62-71) removed. - new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE. - room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner. - Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add). - test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable). - test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references). - 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper FTs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:59:54 -04:00
{# 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>
tray sig-card tooltip: portal w. PRV|NXT pager — TDD Phase 2 of the apps.tooltips integration on the tray. Hovering .tray-sig-card > .sig-stage-card opens #id_tooltip_portal w. an FYI panel that mirrors #id_fan_fyi_panel (Energy / Operation entries cycled via PRV|NXT), but w.o. the stage block, w.o. Reversal entries, & w.o. the fan stage's click-to-dismiss handler — the panel-body click is reserved for future drag-and-drop on .tray-sig-card:active. - _partials/_sig_fyi_panel.html — new partial, the .sig-info + PRV|NXT block extracted out of game_kit.html, _sig_select_overlay.html, & _sea_overlay.html. {% include %}d back from those 3 callers; pure copy-paste extraction (no behavioural change to fan stage, sig select, or sea select). - room.html: .tray-sig-card > .sig-stage-card gains data-energies + data-operations (the only attrs StageCard.buildInfoData reads), keyed off my_tray_sig.energies_json / .operations_json (existing TarotCard properties). - tray-tooltip.js: new sig branch — _showSig() builds the panel inline, paints via StageCard.renderFyi, & wires PRV|NXT cycle handlers; the mousemove union now covers the .fyi-prev / .fyi-next btn rects (the btns hang past the portal's left & right edges) so mouse-over them keeps the panel alive. Click stopPropagation on the btns prevents the panel-body click from reaching anything else. - TrayTooltipSpec: 6 new sig-branch specs (panel structure; first energy entry rendered; PRV|NXT cycling; body click no-dismiss; pointer over btn rects keeps panel alive; pointer outside full union clears). - test_component_tray_tooltip.py: 4 sig FTs (hover populates portal w. Energy/TESTLIBIDO/effect/1-of-2; PRV|NXT cycle; body click does NOT dismiss; mouseleave clears). FT helper note — the sig FT's _hover dispatches a synthetic mouseenter via JS rather than ActionChains.move_to_element, because the role-card & sig-card cells sit side-by-side in the tray grid: the pointer's animated path crosses the role-card on its way to the sig-card & opens the role tooltip mid-flight, which then occludes the sig stage by the time the move lands. Direct dispatch lands the event on the intended trigger w.o. the cross-cell drag-by. 313 epic ITs + 335 Jasmine specs (incl. 6 new) + 6 tray-tooltip FTs all green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:07:33 -04:00
<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 %}
tray sig-card tooltip: portal w. PRV|NXT pager — TDD Phase 2 of the apps.tooltips integration on the tray. Hovering .tray-sig-card > .sig-stage-card opens #id_tooltip_portal w. an FYI panel that mirrors #id_fan_fyi_panel (Energy / Operation entries cycled via PRV|NXT), but w.o. the stage block, w.o. Reversal entries, & w.o. the fan stage's click-to-dismiss handler — the panel-body click is reserved for future drag-and-drop on .tray-sig-card:active. - _partials/_sig_fyi_panel.html — new partial, the .sig-info + PRV|NXT block extracted out of game_kit.html, _sig_select_overlay.html, & _sea_overlay.html. {% include %}d back from those 3 callers; pure copy-paste extraction (no behavioural change to fan stage, sig select, or sea select). - room.html: .tray-sig-card > .sig-stage-card gains data-energies + data-operations (the only attrs StageCard.buildInfoData reads), keyed off my_tray_sig.energies_json / .operations_json (existing TarotCard properties). - tray-tooltip.js: new sig branch — _showSig() builds the panel inline, paints via StageCard.renderFyi, & wires PRV|NXT cycle handlers; the mousemove union now covers the .fyi-prev / .fyi-next btn rects (the btns hang past the portal's left & right edges) so mouse-over them keeps the panel alive. Click stopPropagation on the btns prevents the panel-body click from reaching anything else. - TrayTooltipSpec: 6 new sig-branch specs (panel structure; first energy entry rendered; PRV|NXT cycling; body click no-dismiss; pointer over btn rects keeps panel alive; pointer outside full union clears). - test_component_tray_tooltip.py: 4 sig FTs (hover populates portal w. Energy/TESTLIBIDO/effect/1-of-2; PRV|NXT cycle; body click does NOT dismiss; mouseleave clears). FT helper note — the sig FT's _hover dispatches a synthetic mouseenter via JS rather than ActionChains.move_to_element, because the role-card & sig-card cells sit side-by-side in the tray grid: the pointer's animated path crosses the role-card on its way to the sig-card & opens the role tooltip mid-flight, which then occludes the sig stage by the time the move lands. Direct dispatch lands the event on the intended trigger w.o. the cross-cell drag-by. 313 epic ITs + 335 Jasmine specs (incl. 6 new) + 6 tray-tooltip FTs all green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:07:33 -04:00
{% 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 %}
tray cards: shadow, hover-tilt w. focus persistence, role-card tooltip — TDD - _tray.scss: drop-shadow on cell child elements (img → filter:drop-shadow so the silhouette is the shadow caster, div → box-shadow); 7° hover-tilt on .tray-role-card > img (-7°) and .tray-sig-card > .sig-stage-card (+7° via the standalone `rotate` property so the existing -5° baseline transform composes); :focus persists the tilt after click; cursor: pointer - tray.js: set tabIndex=0 on placeCard's role cell + on template-rendered .tray-role-card / .tray-sig-card cells at init() so :focus latches the hover state; clear tabindex in reset() for Jasmine afterEach - TraySpec: 4 new specs covering placeCard tabindex, reset cleanup, init-time tabindex on template-rendered sig & role cards, no-tabindex on bare cells - New tray-tooltip.js (#id_tooltip_portal) — Phase 1 of the apps.tooltips integration: hovering .tray-role-card > img copies its sibling .tt's innerHTML into the page-root portal, anchors above/below the trigger, & clamps to the viewport horizontally; mousemove outside the union of [trigger, portal] rects clears the portal (Game-Kit pattern, no btns) - room.html: #id_tooltip_portal mounted at room-page root (outside tray's overflow:hidden); .tt block rendered inline inside .tray-role-card via {% tooltip %} templatetag w. title=role display name & description="[Placeholder description]" - epic/views.py: my_tray_role_tooltip context dict ({title, description}) keyed off the seated role - TrayTooltipSpec: 8 specs covering portal population, .active class, sibling-.tt fallback, viewport-edge clamp left/right, and union-rect mouseleave - 2 FTs in test_component_tray_tooltip.py: hover role img → portal title=Player + description=Placeholder; mouseleave → portal clears Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel via a DRY refactor) deferred per plan. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:40:10 -04:00
{% if room.table_status %}
<div id="id_tooltip_portal" class="tt" style="display:none;"></div>
{% endif %}
position circles: rich .tt-pos-* hover tooltips on the gate-view + table circles — @handle/title/seat-sig/shoptalk/#tokens/expiry portal + CARTE me-also ?seat switch href — TDD 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>
2026-06-01 12:13:09 -04:00
{# 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" %}
my_sea_gate burger; _bud_apparatus shared shell; CI #344 tray-anchor fix — TDD Continuation of the burger sprint into the my-sea gatekeeper + a couple companion cleanups the visual + CI runs surfaced. ## my_sea_gate.html burger `templates/apps/gameboard/_partials/_room_burger.html` → `_burger.html` (git mv) — now lives at a non-room-scoped path since it's reused across templates. Updated room.html's include path. `templates/apps/gameboard/my_sea_gate.html` — includes `_burger.html` + loads `burger-btn.js`. Burger renders unconditionally on the my-sea gatekeeper, same affordance as the room gatekeeper. `apps/gameboard/tests/integrated/test_views.py::MySeaGateViewTest` — new `test_gate_view_renders_burger_btn_and_fan` IT asserts burger_btn + burger_fan + 5 sub-btns w. correct ids + burger-btn.js loaded on `/gameboard/my-sea/gate/`. ## Burger fade rule reinstated `static_src/scss/_burger.scss` — `html.bud-open #id_burger_btn { opacity: 0; pointer-events: none; }` reinstated, scoped to landscape only. User-confirmed: even after the z-index drop the burger needs to disappear when bud_panel is open in landscape (panel + bud_ok races vs. burger pointer-events). Portrait keeps burger visible since the panel sits BELOW the burger w. no overlap. ## "friend@example.com" → "bud@example.com" placeholder Renamed in 4 bud-panel templates + the FT asserting it: - `templates/apps/gameboard/_partials/_my_sea_bud_panel.html` - `templates/apps/billboard/_partials/_bud_panel.html` - `templates/apps/billboard/_partials/_bud_invite_panel.html` - `templates/apps/billboard/_partials/_bud_add_panel.html` - `functional_tests/test_core_sharing.py` "bud" matches the broader naming convention (bud_btn, my-buds, share-w.-a-bud, etc.) — `friend` was an outlier. ## _bud_apparatus.html shared shell refactor New `templates/apps/billboard/_partials/_bud_apparatus.html` — single shared markup partial for the four bud-btn use cases. Contains btn + panel + input + OK + (optional) suggestions div + bud-btn.js script. Each of the 4 specific partials becomes a thin wrapper that `{% include %}`s the shell + renders its own `<script>bindBudBtn({...})</script>` block w. per-use-case submitUrl / onSuccess / duplicateTargetSelector. Context vars accepted by the shell: - `aria_label` — string for #id_bud_btn aria-label - `sharer_name` — optional; renders `data-sharer-name=` on #id_bud_panel (post-share only) - `include_suggestions` — bool; renders suggestions div + autocomplete script (false on my_buds where the pool == request.user.buds == nothing useful to suggest) Per-call wrappers are now ~10-50 lines instead of 30-120 lines of duplicate markup. Behaviour is identical; only DRY-ed up. ## CI #344 tray-anchor regression fix `apps/epic/static/apps/epic/tray.js` — `_computeBounds` in landscape used `id_gear_btn || id_kit_btn` as the bottom anchor (wrap height = anchor.top). After the burger sprint relocated kit_btn to the TOP of the right sidebar (top:0.5rem), kit_btn.top ≈ 8px → wrap.height collapsed to 8px → tray couldn't slide → `test_dragging_tray_btn_down_opens_tray_in_landscape` failed. Fix: anchor fallback chain is now `id_burger_btn || id_bud_btn` (the new bottom-anchored btns) → `window.innerHeight - 3.5rem` reserved fallback (for pages that have neither). Burger renders unconditionally on room.html so the SIG_SELECT tray test now finds its anchor + lays out the wrap correctly. ## Verification - IT+UT 1357 green (+1 from MySeaGateViewTest burger). - Jasmine specs green. - `test_dragging_tray_btn_down_opens_tray_in_landscape` green (was the CI #344 failure). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:57:45 -04:00
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
{% endblock content %}
{% block scripts %}
gatekeeper invite ports to #id_bud_btn slide-out: drop the inline #id_invite_email form, add bud-invite panel for room owner during gate phase, async POST to invite_gamer w. autocomplete + symmetric buds auto-add + slide-down Brief banner — TDD - Brief schema (billboard/0007): post FK becomes nullable + new room FK to epic.Room + KIND_GAME_INVITE enum value. to_banner_dict resolves post_url to reverse('epic:gatekeeper', room.id) when post is null and room is set. - epic.invite_gamer view refactor: • Accepts `recipient` (matches bud-panel field; legacy `invitee_email` still works for full backwards compat). • Resolves via apps.billboard.views._resolve_recipient (email if "@" present, else username). • RoomInvite stores the resolved User's email (or raw input if unregistered). • Auto-adds inviter ↔ recipient to each others' buds (symmetric per Phase 2 spec) when recipient is a registered User. • Spawns a Brief w. owner=request.user, kind=GAME_INVITE, room=room, post=null. • Accept: application/json → {brief, recipient_display}; otherwise redirects to gatekeeper as before. • Self-invite + blank recipient: 200 w. brief=null, no RoomInvite, no buds touch. - _gatekeeper.html: gate-invite-panel block (lines 62-71) removed. - new templates/apps/billboard/_partials/_bud_invite_panel.html: clone of _bud_panel.html w. data-invite-url + autocomplete from request.user.buds. JS posts to invite_gamer + Brief.showBanner. room.html includes it owner-only when not table_status and gate_status != RENEWAL_DUE. - room.html scripts block now loads apps/dashboard/note.js so window.Brief is defined for the slide-down banner. - Tests: new test_invite_gamer.py (14 ITs) covering ajax + legacy form-submit paths, recipient resolution, RoomInvite creation, Brief w. room FK + GAME_INVITE kind, symmetric buds auto-add, unregistered/self/blank silent no-op cases. New test_gatekeeper_bud_btn.py FT (9 tests) covers presence (owner-only), absence of legacy #id_invite_email, async invite flow end-to-end (RoomInvite, Brief, banner, panel close, username resolve, buds auto-add). - test_brief.test_brief_owner_post_required relaxed to test_brief_owner_required (post is now nullable). - test_room_gatekeeper.test_second_gamer_drops_token_into_open_slot updated to drive the bud-btn flow (drops the #id_invite_email/#id_invite_btn references). - 866 ITs (+14) + 9 gatekeeper FTs + 28 existing room-gatekeeper FTs green. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:59:54 -04:00
{# 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>
Game Kit fan stage + FLIP/SPIN; sig/sea/fan refactor — TDD - fan modal: stage block w. idle-reveal/careen-out; carousel shifts left so focused card sits left-of-center; SPIN rotates whole card via Element.animate(); FLIP toggles polarity (Levity ↔ Gravity) via perspective rotateY w. mid-flip repaint; SPIN state retained across FLIP; FLIP btn hover-revealed only when focused card or btn is hovered (:has) - mobile breakpoints: --fan-card-w / --fan-card-h / --fan-stage-shift / --fan-carousel-step lifted to CSS vars on .tarot-fan-wrap; portrait ≤ 480px @ 150×230, landscape ≤ 500h @ 150×235; corners + face text/padding scale w. card width - shared StageCard JS module (apps/epic/stage-card.js): fromDataset, populateCard, populateKeywords, buildInfoData, renderFyi — sig/sea/fan all delegate; ~150 lines de-duplicated - shared @mixin stat-block-shared (SCSS) lifts duplicated stat-face / stat-keywords / sig-info rules; @mixin stage-card-polarity unifies sea-stage--levity/--gravity + fan[data-polarity] coloring - model rename: TarotCard.reversal → reversal_qualifier (migration 0014); render-time fallback to current polarity's qualifier when blank - class unification: .sig-info-open / .sea-info-open / .fyi-open → .fyi-open (on stat block); .sig-flip-btn / .sea-spin-btn / .fan-spin-btn → .spin-btn; same for .fyi-btn / .fyi-prev / .fyi-next - custom combobox (apps/epic/combobox.js) replaces native <select> for PICK SEA spread picker — keyboard nav, click-outside-close, aria roles; Firefox/Chrome OS-rendered <option> ignored CSS - Jasmine: FanStageSpec.js w. idle-reveal / population / SPIN / FYI / FLIP specs; sig + sea fixtures + IT view assertions updated for renamed classes - 748 ITs + Jasmine green Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:01:52 -04:00
<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>
tray cards: shadow, hover-tilt w. focus persistence, role-card tooltip — TDD - _tray.scss: drop-shadow on cell child elements (img → filter:drop-shadow so the silhouette is the shadow caster, div → box-shadow); 7° hover-tilt on .tray-role-card > img (-7°) and .tray-sig-card > .sig-stage-card (+7° via the standalone `rotate` property so the existing -5° baseline transform composes); :focus persists the tilt after click; cursor: pointer - tray.js: set tabIndex=0 on placeCard's role cell + on template-rendered .tray-role-card / .tray-sig-card cells at init() so :focus latches the hover state; clear tabindex in reset() for Jasmine afterEach - TraySpec: 4 new specs covering placeCard tabindex, reset cleanup, init-time tabindex on template-rendered sig & role cards, no-tabindex on bare cells - New tray-tooltip.js (#id_tooltip_portal) — Phase 1 of the apps.tooltips integration: hovering .tray-role-card > img copies its sibling .tt's innerHTML into the page-root portal, anchors above/below the trigger, & clamps to the viewport horizontally; mousemove outside the union of [trigger, portal] rects clears the portal (Game-Kit pattern, no btns) - room.html: #id_tooltip_portal mounted at room-page root (outside tray's overflow:hidden); .tt block rendered inline inside .tray-role-card via {% tooltip %} templatetag w. title=role display name & description="[Placeholder description]" - epic/views.py: my_tray_role_tooltip context dict ({title, description}) keyed off the seated role - TrayTooltipSpec: 8 specs covering portal population, .active class, sibling-.tt fallback, viewport-edge clamp left/right, and union-rect mouseleave - 2 FTs in test_component_tray_tooltip.py: hover role img → portal title=Player + description=Placeholder; mouseleave → portal clears Phase 2 (sig-card tooltip mirroring #id_fan_fyi_panel via a DRY refactor) deferred per plan. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:40:10 -04:00
<script src="{% static 'apps/epic/tray-tooltip.js' %}"></script>
position circles: rich .tt-pos-* hover tooltips on the gate-view + table circles — @handle/title/seat-sig/shoptalk/#tokens/expiry portal + CARTE me-also ?seat switch href — TDD 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>
2026-06-01 12:13:09 -04:00
<script src="{% static 'apps/epic/position-tooltip.js' %}"></script>
room.html burger btn + 5-fan; universal landscape btn refactor; kit_bag_dialog vertical bar — TDD Major feature push staging room.html for sprint A.8 — adds the burger btn + fan-of-five sub-btns affordance, then rotates the whole landscape btn layout to make room for it. The landscape refactor is universal (not .room-page-scoped) so every page that hosts these btns reads consistently. ## Burger btn + fan-of-five (room.html only) `templates/apps/gameboard/_partials/_room_burger.html` (NEW) — `#id_burger_btn` (.fa-burger) + `#id_burger_fan` containing 5 sub-btns: `#id_voice_btn` (headset), `#id_sky_btn` (cloud), `#id_earth_btn` (earth-americas), `#id_sea_btn` (bridge-water), `#id_text_btn` (keyboard). Pure scaffolding — no click handlers in this sprint; wire-up lands later as each surface matures. `apps/epic/static/apps/epic/burger-btn.js` (NEW) — toggle `.active` on click; Escape + click-outside close; opening burger auto-closes the kit dialog (`#id_kit_bag_dialog`) + the bud slide-out panel (`html.bud-open`) by dispatching a click to the owning btn (routes through that btn's own toggle/close path — no fetch on close). `bindBurger()` returns an `AbortController` so test code (+ any future re-bind callers) can detach all listeners cleanly via `ac.abort()`. `static_src/scss/_burger.scss` (NEW) — burger sits parallel to gear-above-kit but on the bud-side. Portrait: bottom:4.2rem; left:0.5rem (above #id_bud_btn). Landscape: bottom:0.5rem; right:4.2rem (to the LEFT of #id_bud_btn). Fan is a CSS radial menu w. each sub-btn's `--angle = --base + --i * 30deg`. Portrait `--base: 0deg` (arc 12→4 o'clock), landscape `--base: -90deg` (arc 9→1 o'clock). Sub-btn radius `--r: 7.75rem`. Indices: voice=0, sky=1, earth=2, sea=3, text=4 (user-spec'd clockwise order). room.html includes the partial + the script; the burger renders unconditionally regardless of `gate_status` / `table_status`. ## Universal landscape btn refactor Sprint replaces the prior centred-in-sidebar landscape arrangement w. a kit-at-top + bud-at-bottom + gear/burger as horizontal partners. Applies to every page in landscape (was scoped to .room-page in the first iteration). `_game-kit.scss` — kit_btn landscape moves to top:0.5rem; right:0.5rem (was bottom:0.5rem centred in sidebar). The 0.5rem right literal (not the calc((--sidebar-w - 3rem)/2)=1rem) produces a 0.7rem edge-to-edge gap w. gear at right:4.2rem — matching the portrait gear-above-kit gap exactly. `_bud.scss` — bud_btn landscape moves to bottom:0.5rem; right:0.5rem (was top:0.5rem centred in sidebar). Same 0.5rem literal as kit_btn. bud_panel #id_recipient relocates to bottom:0.5rem (was top:0.5rem) + transform-origin flips to right center. .bud-suggestions rise upward from above the panel (bottom:4rem) instead of dropping from below. `_applets.scss` — .gear-btn landscape moves to top:0.5rem; right:4.2rem (was centred bottom:3.95rem). All applet menus anchor at top:2.6rem; right:4.2rem (beneath the gear's leftward arc) extending DOWN-LEFT into the viewport. #id_room_menu joins the shared portrait position list (was bespoke in _room.scss). `_room.scss` — bespoke #id_room_menu rule deleted entirely. The menu now inherits %applet-menu + the shared portrait position list — same chrome + behaviour as #id_post_menu / #id_billscroll_menu. Earlier iteration tried flex-direction:row in landscape; reverted per user request — "lose all scss specificity" wins. `_card-deck.scss` — obsolete `#id_room_menu { right: 2.5rem; }` override in the XL+landscape block deleted. Was a same-specificity hack to beat _applets.scss's old centred position; no longer needed w. the consolidated rule. ## kit_bag_dialog vertical bar in landscape `_game-kit.scss` — when open in landscape, dialog covers the right sidebar (top:0; bottom:0; right:0; width: var(--sidebar-w)). Slides in from off-viewport right by animating max-width 0 → var(--sidebar-w). Opaque bg (rgba(--priUser, 1) — was 0.97). z-index: 319 (above burger at 318) so it lands in front of the burger btn when open. Top-edge border → left-edge border. Inner content flips to `flex-direction: column-reverse` so DOM order Deck→Dice→Trinket→Tokens paints visually bottom→top. .kit-bag-section also column-reverse → icon row above label. .kit-bag-label drops vertical-rl + the rotate(180deg) scaleX(1.3) transform, reads horizontally. .kit-bag-row--scroll flips to column + overflow-y for the Tokens scrollable row. `game-kit.js attachTooltip()` — 2-axis tooltip clamp matching sky-wheel.js + wallet.js's pattern. Horizontal: left edge stays within [1rem, viewport-ttW-1rem]. Vertical: prefer ABOVE the element; flip BELOW when tooltip is too tall to fit above (e.g. landscape kit bar w. Tokens row near top). Resets top/bottom on mouseleave so next show measures fresh. ## Tests `apps/epic/tests/integrated/test_views.py` (+1 class, 6 ITs) — RoomBurgerBtnRenderTest: burger_btn renders, fan container renders, 5 sub-btns w. correct ids, icons match spec, burger-btn.js loaded, burger persists thru table_status. `static_src/tests/BurgerSpec.js` (NEW, 19 Jasmine specs) — bindBurger() returns AbortController; click toggle; Escape close; click-outside close; opening burger closes kit dialog + bud panel when set; AbortController teardown removes listeners. All 1356 IT+UT green (+6 new from RoomBurgerBtnRenderTest, was 1350). Jasmine suite green (was 220-something specs, +19 BurgerSpec). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 20:40:33 -04:00
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
{% endblock scripts %}