my_sea_gate burger; _bud_apparatus shared shell; CI #344 tray-anchor fix — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

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>
This commit is contained in:
Disco DeDisco
2026-05-26 21:57:45 -04:00
parent 03feaee9f2
commit 6809681e5a
12 changed files with 102 additions and 123 deletions

View File

@@ -100,14 +100,20 @@ var Tray = (function () {
// meets the gear button when open. Tray is flex:1 and fills the rest.
// Open: wrap top = 0 (pinned to viewport top).
// Closed: wrap top = -(gearBtnTop - handleH) = tray fully above viewport.
// Anchor: id_gear_btn historically; id_kit_btn is the live fallback so
// the open-state handle bottom lands at the bottom-right anchor instead
// of overlapping it (no id_gear_btn renders on the room page today).
var gearBtn = document.getElementById('id_gear_btn')
|| document.getElementById('id_kit_btn');
var gearBtnTop = window.innerHeight;
if (gearBtn) {
gearBtnTop = Math.round(gearBtn.getBoundingClientRect().top);
// Anchor: the bottom-most btn on the right sidebar in landscape.
// After the burger sprint, kit_btn + gear_btn relocated to the TOP
// of the sidebar — so using them as anchors collapses wrap height
// to ~8px. Burger (room.html) + bud (post.html) are now the only
// btns reliably anchored at the BOTTOM. Fall back to a fixed
// reserved 3.5rem if neither is present on the page.
var anchor = document.getElementById('id_burger_btn')
|| document.getElementById('id_bud_btn');
var gearBtnTop;
if (anchor) {
gearBtnTop = Math.round(anchor.getBoundingClientRect().top);
} else {
var rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
gearBtnTop = window.innerHeight - 3.5 * rem;
}
// handleH = #id_tray_handle's height (48px CSS), NOT _btn.offsetHeight.
// The 3rem btn can be larger than the 48px handle on big-rem viewports

View File

@@ -2154,6 +2154,17 @@ class MySeaGateViewTest(TestCase):
self.assertContains(response, "id_my_sea_paid_draw_btn")
self.assertContains(response, reverse("my_sea_refund_token"))
def test_gate_view_renders_burger_btn_and_fan(self):
response = self.client.get(reverse("my_sea_gate"))
self.assertContains(response, 'id="id_burger_btn"')
self.assertContains(response, 'id="id_burger_fan"')
for btn_id in (
"id_voice_btn", "id_sky_btn", "id_earth_btn",
"id_sea_btn", "id_text_btn",
):
self.assertContains(response, f'id="{btn_id}"')
self.assertContains(response, "burger-btn.js")
class MySeaInsertTokenViewTest(TestCase):
"""Sprint 6 iter 6a — POST `/gameboard/my-sea/insert` reserves the

View File

@@ -41,7 +41,7 @@ class SharingTest(FunctionalTest):
share_box = post_page.get_share_box()
self.assertEqual(
share_box.get_attribute("placeholder"),
"friend@example.com or username",
"bud@example.com or username",
)
post_page.share_post_with("alice@test.io")

View File

@@ -123,8 +123,15 @@
pointer-events: auto;
}
// No explicit bud_panel / kit_dialog hide rules needed — burger's z=314
// sits BELOW kit_btn / bud_btn (318), bud_panel (317), + kit_dialog (316),
// so all those naturally cover the burger when they overlap. Earlier
// iterations explicitly faded the burger via opacity; rendered redundant
// by the z-index drop.
// Burger hides when bud_panel is open — LANDSCAPE only. In portrait the
// burger sits ABOVE the bud panel (bottom:4.2rem vs panel at bottom:0.5
// + height:3rem); no visual conflict. In landscape they share the
// bottom-right area + the bud OK btn ends up obscured / pointer-events
// races against the burger even at the lower z-index — explicit fade
// keeps the bud_panel apparatus clean.
@media (orientation: landscape) {
html.bud-open #id_burger_btn {
opacity: 0;
pointer-events: none;
}
}

View File

@@ -1,30 +1,8 @@
{% load static %}
{# ─────────────────────────────────────────────────────────────────────── #}
{# _bud_add_panel.html — bottom-left handshake btn + slide-out add-bud #}
{# field. Skeleton (open/close/POST) owned by bud-btn.js; this partial #}
{# wires the add-bud success callback: {bud} → append .bud-entry to #}
{# #id_buds_list. #}
{# Included by my_buds.html only. #}
{# #}
{# No autocomplete — the bud-autocomplete pool is request.user.buds, #}
{# which is precisely the set you can't usefully re-add. Post-share + #}
{# gatekeeper-invite panels DO bind autocomplete. #}
{# ─────────────────────────────────────────────────────────────────────── #}
<button id="id_bud_btn" type="button" aria-label="Add a bud">
<i class="fa-solid fa-handshake"></i>
</button>
<div id="id_bud_panel">
<input id="id_recipient"
name="recipient"
type="text"
placeholder="friend@example.com or username"
autocomplete="off">
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
</div>
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
{# _bud_add_panel.html — handshake btn + slide-out add-bud field on #}
{# my_buds.html. No autocomplete — the pool is request.user.buds, which #}
{# is precisely the set you can't usefully re-add. On success, appends #}
{# a .bud-entry to #id_buds_list. #}
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Add a bud" include_suggestions=False %}
<script>
(function () {
'use strict';

View File

@@ -0,0 +1,43 @@
{% load static %}
{# ─────────────────────────────────────────────────────────────────────── #}
{# _bud_apparatus.html — shared markup for the four bud-btn use cases: #}
{# • _my_sea_bud_panel.html (my-sea gatekeeper invite stub) #}
{# • _bud_panel.html (post.html share-post) #}
{# • _bud_invite_panel.html (room.html gatekeeper game-invite) #}
{# • _bud_add_panel.html (my_buds.html add-bud) #}
{# #}
{# Each caller follows the include w. its own #}
{# <script>bindBudBtn({...})</script> block carrying the per-use-case #}
{# submitUrl + onSuccess + (optional) duplicateTargetSelector. The shell #}
{# loads bud-btn.js + autocomplete (when `include_suggestions` is true) #}
{# so the caller can call bindBudBtn() inline. #}
{# #}
{# Context vars: #}
{# 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 #id_bud_suggestions + autocompete #}
{# script (false only for my_buds, where the #}
{# autocomplete pool == request.user.buds, which #}
{# is precisely what you can't usefully re-add) #}
{# ─────────────────────────────────────────────────────────────────────── #}
<button id="id_bud_btn" type="button" aria-label="{{ aria_label|default:'Bud' }}">
<i class="fa-solid fa-handshake"></i>
</button>
<div id="id_bud_panel"{% if sharer_name %} data-sharer-name="{{ sharer_name }}"{% endif %}>
<input id="id_recipient"
name="recipient"
type="text"
placeholder="bud@example.com or username"
autocomplete="off">
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
</div>
{% if include_suggestions %}
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
{% endif %}
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>

View File

@@ -1,33 +1,8 @@
{% load static %}
{# ─────────────────────────────────────────────────────────────────────── #}
{# _bud_invite_panel.html — bud btn + slide-out for the gatekeeper game- #}
{# invite flow. Skeleton (open/close/POST + autocomplete) owned by #}
{# bud-btn.js; this partial wires the invite success callback: server #}
{# returns {brief, recipient_display} — no line_text (no Post to append a #}
{# Line to). JS just shows the Brief banner. #}
{# #}
{# Caller must pass `room` in context. #}
{# ─────────────────────────────────────────────────────────────────────── #}
<button id="id_bud_btn" type="button" aria-label="Invite a friend">
<i class="fa-solid fa-handshake"></i>
</button>
<div id="id_bud_panel">
<input id="id_recipient"
name="recipient"
type="text"
placeholder="friend@example.com or username"
autocomplete="off">
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
</div>
{# Autocomplete suggestions — sibling because the panel has overflow:hidden #}
{# for the slide-in scaleX animation. Pulls from request.user.buds. #}
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
{# _bud_invite_panel.html — bud btn + slide-out for the gatekeeper #}
{# game-invite flow on room.html. Server returns {brief, #}
{# recipient_display} — no line_text (no Post to append a Line to). JS #}
{# just shows the Brief banner. Caller must pass `room` in context. #}
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Invite a friend" include_suggestions=True %}
<script>
bindBudBtn({
submitUrl: '{% url "epic:invite_gamer" room.id %}',

View File

@@ -1,36 +1,11 @@
{% load static %}
{% load lyric_extras %}
{# ─────────────────────────────────────────────────────────────────────── #}
{# _bud_panel.html — bottom-left handshake btn + slide-out recipient #}
{# field for the share-post async flow. Skeleton (open/close/POST + auto- #}
{# complete) owned by bud-btn.js; this partial wires the share-post #}
{# success callback: appends a Line to #id_post_table, fires Brief.show- #}
{# Banner, and updates the .post-header shared-with prose. #}
{# Included by post.html only. #}
{# #}
{# Spec lives in functional_tests/test_bud_btn.py. #}
{# ─────────────────────────────────────────────────────────────────────── #}
<button id="id_bud_btn" type="button" aria-label="Share with a bud">
<i class="fa-solid fa-handshake"></i>
</button>
<div id="id_bud_panel"
data-sharer-name="{% if request.user.is_authenticated %}{{ request.user|at_handle }}{% endif %}">
<input id="id_recipient"
name="recipient"
type="text"
placeholder="friend@example.com or username"
autocomplete="off">
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
</div>
{# Autocomplete suggestions — sibling of #id_bud_panel because the panel #}
{# has overflow:hidden for its scaleX slide animation. #}
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
{# _bud_panel.html — bud btn + slide-out for the post-share flow on #}
{# post.html. On success: appends a Line to #id_post_table, fires #}
{# Brief.showBanner, + updates the .post-header shared-with prose. #}
{# `sharer_name` (rendered onto data-sharer-name) is the author of the #}
{# optimistically-appended Line so it matches the persisted post-refresh #}
{# state where Line.author == request.user. #}
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Share with a bud" sharer_name=request.user|at_handle include_suggestions=True %}
<script>
(function () {
'use strict';

View File

@@ -1,26 +1,8 @@
{% load static %}
{# Sprint 6 iter 6c — bud-btn invite panel on the my-sea gatekeeper. #}
{# Mirrors `_bud_invite_panel.html` (room) but POSTs to a stub view — #}
{# real async multi-user invite is deferred to a future sprint, so OK #}
{# returns a 'Multiplayer my-sea coming soon' Brief banner. #}
<button id="id_bud_btn" type="button" aria-label="Invite a friend">
<i class="fa-solid fa-handshake"></i>
</button>
<div id="id_bud_panel">
<input id="id_recipient"
name="recipient"
type="text"
placeholder="friend@example.com or username"
autocomplete="off">
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
</div>
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
{% include "apps/billboard/_partials/_bud_apparatus.html" with aria_label="Invite a friend" include_suggestions=True %}
<script>
bindBudBtn({
submitUrl: '{% url "my_sea_invite" %}',

View File

@@ -89,6 +89,7 @@
{# the gear-btn sits atop `#id_kit_btn` in the bottom-right corner. #}
{% include "apps/gameboard/_partials/_my_sea_bud_panel.html" %}
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
{% endblock content %}
@@ -97,4 +98,5 @@
{# 'Multiplayer my-sea coming soon' banner shows on a successful #}
{# (stub) invite. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
{% endblock scripts %}

View File

@@ -119,7 +119,7 @@
<div id="id_tooltip_portal" class="tt" style="display:none;"></div>
{% endif %}
{% include "apps/gameboard/_partials/_room_gear.html" %}
{% include "apps/gameboard/_partials/_room_burger.html" %}
{% include "apps/gameboard/_partials/_burger.html" %}
</div>
{% endblock content %}