A.7.5-polish-6 FLIP btn everywhere — applet gate dropped + sea_stage modal gets FLIP. User-spec 2026-05-25 PM ("If it's interfering to have bespoke rules, just allow the FLIP btn everywhere, including in my_sea.html") follow-up to polish-5 (1e2041e).

**(1) `_applet-my-sign.html`** — FLIP btn moved OUTSIDE the `{% if card.deck_variant.has_card_images %}` + nested `{% if not card.deck_variant.is_polarized %}` gates. Now renders as a direct child of `.my-sign-applet-card` for ALL cards regardless of mode/polarity. Back-img element stays gated (back-img is meaningless for polarized decks or text-mode — would render an empty src). JS handler in the same template ungated too (was wrapped in matching `{% if %}` blocks); now always wires + gracefully no-ops on click when no `.sig-stage-card-back-img` sibling exists. Card-element selector broadened from `.my-sign-applet-card--image` (image-mode only) to `.my-sign-applet-card` (any mode).

**(2) `_sea_stage.html`** — added `<img class="sig-stage-card-back-img">` (gated on `request.user.equipped_deck.has_card_images and not is_polarized` — same condition as my_sign.html's main page back-img) + `<button class="sea-stage-flip-btn">` (unconditional). Both nested INSIDE the `.sig-stage-card.sea-stage-card` for card-relative positioning. Multi-user gameroom is a known limitation here — the back-img src is the room viewer's deck-back, not the drawing gamer's, which is wrong when different gamers' decks have different backs. Parked for a future multi-user polish pass (called out in template comment).

**(3) `_card-deck.scss`** — extended the polish-5 shared FLIP-btn rule trio (positioning + hover-reveal + mid-flip-hide) to include `.sea-stage-flip-btn` across all 3 declarations. Now all 4 surfaces (my_sign main / applet / sea_stage / fan carousel) share the same opacity-0-default + hover-reveal + display:none-mid-flip behavior — single source of truth.

**(4) `sea.js`** — added FLIP btn click handler in the init() function next to the existing SPIN/FYI handlers. Mirrors the `_flipToBackAnimated` shape from my_sign.html / _applet-my-sign.html: rotateY 0→90→0 over 500ms, toggle `.is-flipped-to-back` at midpoint, `[data-flipping]` attr for SCSS mid-flip-hide. Same defensive no-op pattern as the applet — bails when no `.sig-stage-card-back-img` sibling exists. Behavior for polarized text-mode decks (no back-img rendered): click is a no-op. Polarized image-mode (future Earthman art): also no-op since back-img is server-gated to non-polarized. Non-polarized image-mode (Minchiate today): flips between front + back.

**Why ungate the FLIP btn rendering rather than render it conditionally per surface:** user-spec was "just allow the FLIP btn everywhere" + the prior bespoke per-surface gating was causing both visual quirks (missing FLIP btn in applet earlier) + maintenance complexity. The unified "always render, JS picks behavior by sibling existence" pattern eliminates the per-surface conditional templates. The btn is always visible-on-hover, always click-handles cleanly, gracefully no-ops where it has nothing to flip to — minimal surprise, maximal consistency.

**JS handlers not unified into a shared module** (yet): each of the 3 surfaces (my_sign main inline script, applet inline script, sea.js init()) carries its own copy of the ~15-line FLIP-to-back animate-and-toggle dance. Could be DRY'd into a `StageCard.flipToBack(card, btn)` helper at some point, but the call sites differ enough in setup (different parent DOM selectors, different surrounding state — frozen-gate for my_sign, no gate for applet/sea_stage) that the helper would mostly be the animate+setTimeout block. Deferred — flagged in [[project-image-based-deck-face-rendering]] follow-ups if it accretes.

Tests: 1314/1314 IT+UT total green (71s). No new tests — JS handler change is pure DOM augmentation; template changes just relax server-side gates (no new conditionals to test). Visual verify 2026-05-25 PM via Claudezilla on /billboard/: applet FLIP btn present (opacity:0 at rest, hover-reveals); shared `.my-sign-applet-card:hover .my-sign-applet-flip-btn` CSS rule confirmed in computed stylesheet; my_sign main page FLIP behavior unchanged (still works per user 2026-05-25 PM "Works well in my_sign.html tho").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 19:31:45 -04:00
parent 1e2041ed9f
commit b308115fcf
4 changed files with 109 additions and 43 deletions

View File

@@ -227,6 +227,31 @@ var SeaDeal = (function () {
}); });
} }
// Polish-6 — FLIP btn (rotateY 0→90→0 over 500ms, toggle
// `.is-flipped-to-back` at midpoint). Renders unconditionally per
// user-spec "allow the FLIP btn everywhere"; no-ops when the card
// has no `.sig-stage-card-back-img` sibling (back-img only renders
// server-side for non-polarized image-equipped decks; text-mode +
// polarized image decks render no back-img + the FLIP is inert).
// Mirrors `_flipToBackAnimated` shape from my_sign.html / applet.
var flipBtn = stage.querySelector('.sea-stage-flip-btn');
if (flipBtn) {
flipBtn.addEventListener('click', function () {
if (stageCard.dataset.flipping) return;
if (!stageCard.querySelector('.sig-stage-card-back-img')) return;
stageCard.dataset.flipping = '1';
stageCard.animate([
{ transform: 'rotateY(0deg)' },
{ transform: 'rotateY(90deg)', offset: 0.5 },
{ transform: 'rotateY(0deg)' },
], { duration: 500, easing: 'ease' });
setTimeout(function () {
stageCard.classList.toggle('is-flipped-to-back');
}, 250);
setTimeout(function () { delete stageCard.dataset.flipping; }, 500);
});
}
// Clicking the FYI panel itself dismisses it (same as sig-select caution) // Clicking the FYI panel itself dismisses it (same as sig-select caution)
if (fyiPanel) { if (fyiPanel) {
fyiPanel.addEventListener('click', function (e) { fyiPanel.addEventListener('click', function (e) {

View File

@@ -989,12 +989,15 @@ html:has(.sig-backdrop) {
background: rgba(var(--duoUser), 1); background: rgba(var(--duoUser), 1);
} }
// Polish-5: my_sign main + applet FLIP btns share one positioning rule. // Polish-5: my_sign main + applet + sea_stage FLIP btns share one positioning
// Both live INSIDE their card (`.sig-stage-card` / `.my-sign-applet-card`, // rule. All live INSIDE their card (`.sig-stage-card` / `.my-sign-applet-card`
// both `position: relative`) so `bottom: 0.6rem; left: 0.6rem` anchors // / `.sig-stage-card.sea-stage-card`, all `position: relative`) so `bottom:
// universally to card-bottom-left w/o needing surface-specific calc(). // 0.6rem; left: 0.6rem` anchors universally to card-bottom-left w/o needing
// surface-specific calc(). Polish-6 added sea-stage-flip-btn for the sea_stage
// modal per user-spec "allow the FLIP btn everywhere".
.my-sign-flip-btn, .my-sign-flip-btn,
.my-sign-applet-flip-btn { .my-sign-applet-flip-btn,
.sea-stage-flip-btn {
@include flip-btn-base; @include flip-btn-base;
z-index: 25; z-index: 25;
bottom: 0.6rem; bottom: 0.6rem;
@@ -1012,19 +1015,22 @@ html:has(.sig-backdrop) {
.my-sign-stage.sig-stage--frozen .sig-stage-card:hover .my-sign-flip-btn, .my-sign-stage.sig-stage--frozen .sig-stage-card:hover .my-sign-flip-btn,
.my-sign-stage.sig-stage--frozen .sig-stage-card:has(.my-sign-flip-btn:hover) .my-sign-flip-btn, .my-sign-stage.sig-stage--frozen .sig-stage-card:has(.my-sign-flip-btn:hover) .my-sign-flip-btn,
.my-sign-applet-card:hover .my-sign-applet-flip-btn, .my-sign-applet-card:hover .my-sign-applet-flip-btn,
.my-sign-applet-card:has(.my-sign-applet-flip-btn:hover) .my-sign-applet-flip-btn { .my-sign-applet-card:has(.my-sign-applet-flip-btn:hover) .my-sign-applet-flip-btn,
.sea-stage-card:hover .sea-stage-flip-btn,
.sea-stage-card:has(.sea-stage-flip-btn:hover) .sea-stage-flip-btn {
@extend %flip-btn-revealed; @extend %flip-btn-revealed;
} }
// Unified mid-flip-hide across all 3 surfaces. `[data-flipping]="1"` is set on // Unified mid-flip-hide across all 4 surfaces. `[data-flipping]="1"` is set on
// the card by each surface's FLIP handler for the 500ms rotation duration; // the card by each surface's FLIP handler for the 500ms rotation duration;
// `%flip-btn-mid-flip`'s `display: none` makes the btn vanish INSTANTLY (per // `%flip-btn-mid-flip`'s `display: none` makes the btn vanish INSTANTLY (per
// user spec — no ease-out logic competing w. the click). Selector chains // user spec — no ease-out logic competing w. the click). Selector chains
// differ per surface because the btn-to-card DOM relationship varies (btn is // differ per surface because the btn-to-card DOM relationship varies (btn is
// INSIDE the card on my_sign + applet post-polish-5; sibling under .tarot- // INSIDE the card on my_sign + applet + sea_stage; sibling under .tarot-fan-
// fan-wrap for the fan). // wrap for the fan carousel).
.sig-stage-card[data-flipping] .my-sign-flip-btn, .sig-stage-card[data-flipping] .my-sign-flip-btn,
.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn, .my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn,
.sea-stage-card[data-flipping] .sea-stage-flip-btn,
.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn { .tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn {
@extend %flip-btn-mid-flip; @extend %flip-btn-mid-flip;
} }

View File

@@ -18,23 +18,26 @@
data-card-id="{{ card.id }}" data-card-id="{{ card.id }}"
data-arcana-key="{{ card.arcana }}"> data-arcana-key="{{ card.arcana }}">
{% if card.deck_variant.has_card_images %} {% if card.deck_variant.has_card_images %}
{# Sprint A.6 — image-mode render mirrors my_sign.html's #} {% comment %}
{# .sig-stage-card--image treatment. Shares the SCSS rule #} Sprint A.6 — image-mode render mirrors my_sign.html's
{# (comma-list selector) so the contour stroke + tray-card #} .sig-stage-card--image treatment. Shares the SCSS rule
{# silhouette black depth shadow + arcana stroke-color #} (comma-list selector) so the contour stroke + tray-card
{# come for free. #} silhouette black depth shadow + arcana stroke-color
come for free.
{% endcomment %}
<img class="sig-stage-card-img" src="{{ card.image_url }}" alt="{{ card.name }}"> <img class="sig-stage-card-img" src="{{ card.image_url }}" alt="{{ card.name }}">
{% if not card.deck_variant.is_polarized %} {% if not card.deck_variant.is_polarized %}
{# Non-polarized image deck: FLIP btn shows the deck back #} {% comment %}
{# image (same behavior as my_sign.html main page). Both #} Non-polarized image deck: back-img element renders the
{# the back-img + flip-btn nest INSIDE the card so the #} deck-back PNG that FLIP toggles to. Back-img stays gated
{# absolute-positioned FLIP btn anchors to the card's #} (back-img is meaningless for polarized decks or text-mode);
{# bounds (card is position: relative in --image mode). #} the FLIP btn itself moved OUT of this gate in polish-6 so
it renders for every card (per user-spec "allow the FLIP
btn everywhere"). The JS click handler is a no-op when no
back-img sibling exists.
{% endcomment %}
<img class="sig-stage-card-back-img" alt="" <img class="sig-stage-card-back-img" alt=""
src="{{ card.deck_variant.back_image_url }}"> src="{{ card.deck_variant.back_image_url }}">
<button class="btn btn-reveal my-sign-applet-flip-btn"
type="button"
aria-label="Flip to deck back">FLIP</button>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="fan-card-corner fan-card-corner--tl"> <div class="fan-card-corner fan-card-corner--tl">
@@ -42,15 +45,17 @@
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %} {% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
</div> </div>
<div class="fan-card-face"> <div class="fan-card-face">
{# `request.user.sig_face` is the rendering payload from #} {% comment %}
{# `TarotCard.applet_face()` — mirrors `populateCard` in #} `request.user.sig_face` is the rendering payload from
{# `stage-card.js:135-144`: #} `TarotCard.applet_face()` — mirrors `populateCard` in
{# • Polarity-split (cards 48-49, trumps 19-21): #} `stage-card.js:135-144`:
{# single-line title, qualifier blank. #} • Polarity-split (cards 48-49, trumps 19-21):
{# • Major + qualifier: title carries a trailing #} single-line title, qualifier blank.
{# comma + qualifier renders BELOW. #} • Major + qualifier: title carries a trailing
{# • Non-Major (middle court, Schizo / Nomad w. no #} comma + qualifier renders BELOW.
{# qualifier): qualifier renders ABOVE the title. #} • Non-Major (middle court, Schizo / Nomad w. no
qualifier): qualifier renders ABOVE the title.
{% endcomment %}
{% with face=request.user.sig_face %} {% with face=request.user.sig_face %}
{% if face.qualifier_first %} {% if face.qualifier_first %}
<p class="fan-card-qualifier">{{ face.qualifier }}</p> <p class="fan-card-qualifier">{{ face.qualifier }}</p>
@@ -67,6 +72,16 @@
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %} {% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
</div> </div>
{% endif %} {% endif %}
{% comment %}
Polish-6 — FLIP btn rendered UNCONDITIONALLY (was gated on
`has_card_images and not is_polarized`). Per user-spec
2026-05-25 PM "just allow the FLIP btn everywhere". JS
handler below picks behavior by sibling existence: back-img
present → flip-to-back animation; absent → no-op.
{% endcomment %}
<button class="btn btn-reveal my-sign-applet-flip-btn"
type="button"
aria-label="Flip card">FLIP</button>
</div> </div>
{# Stat block — same shape as my_sign.html's `.sig-stat-block` #} {# Stat block — same shape as my_sign.html's `.sig-stat-block` #}
{# (Emanation face label + keyword list) but no SPIN/FYI btns #} {# (Emanation face label + keyword list) but no SPIN/FYI btns #}
@@ -82,25 +97,28 @@
{% endcomment %} {% endcomment %}
{% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" card=card %} {% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" card=card %}
</div> </div>
{# Sprint A.6 — applet FLIP btn handler. Mirrors my_sign.html's #} {% comment %}
{# `_flipToBackAnimated()` shape (rotateY 0→90→0 over 500ms, class #} Polish-6 — applet FLIP btn handler. Mirrors my_sign.html's
{# toggle at halfway, `data-flipping` attr for SCSS to hide the #} `_flipToBackAnimated()` (rotateY 0→90→0 over 500ms, class
{# btn). Self-contained inline script — no shared module needed #} toggle at halfway, `data-flipping` attr for SCSS to hide the
{# since the applet is the only consumer outside the main page #} btn during animation). Script is now UNGATED (was conditional
{# (which has its own copy). Script wrapped inside the sig-present #} on `has_card_images and not is_polarized` like the FLIP btn
{# branch AND inside `{% with card %}` scope so `card` references #} itself); per user-spec "allow the FLIP btn everywhere", the
{# resolve + the JS selector strings don't leak into the no-sig #} handler always wires + gracefully no-ops when there's no
{# DOM (which would trip substring-matching tests). #} `.sig-stage-card-back-img` sibling to toggle.
{% if card.deck_variant.has_card_images and not card.deck_variant.is_polarized %} {% endcomment %}
<script> <script>
(function () { (function () {
var applet = document.getElementById('id_applet_my_sign'); var applet = document.getElementById('id_applet_my_sign');
if (!applet) return; if (!applet) return;
var c = applet.querySelector('.my-sign-applet-card--image'); var c = applet.querySelector('.my-sign-applet-card');
var b = applet.querySelector('.my-sign-applet-flip-btn'); var b = applet.querySelector('.my-sign-applet-flip-btn');
if (!c || !b) return; if (!c || !b) return;
b.addEventListener('click', function () { b.addEventListener('click', function () {
if (c.dataset.flipping) return; if (c.dataset.flipping) return;
// No-op when no back-img to toggle (text-mode +
// polarized image-mode decks render no back-img).
if (!c.querySelector('.sig-stage-card-back-img')) return;
c.dataset.flipping = '1'; c.dataset.flipping = '1';
var rest = 'rotateY(0deg)'; var rest = 'rotateY(0deg)';
var mid = 'rotateY(90deg)'; var mid = 'rotateY(90deg)';
@@ -120,7 +138,6 @@
}); });
}()); }());
</script> </script>
{% endif %}
{% endwith %} {% endwith %}
{% else %} {% else %}
<p class="my-sign-applet-empty">No sign chosen yet.</p> <p class="my-sign-applet-empty">No sign chosen yet.</p>

View File

@@ -41,6 +41,24 @@
<span class="fan-corner-rank"></span> <span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i> <i class="fa-solid stage-suit-icon" style="display:none"></i>
</div> </div>
{% comment %}
Polish-6 — back-img + FLIP btn. The back-img mirrors the
my_sign.html / _applet-my-sign.html pattern: only renders
for non-polarized image-equipped decks (since back-img is
meaningless otherwise; src derives from user's equipped
deck). FLIP btn renders UNCONDITIONALLY per user-spec "allow
the FLIP btn everywhere"; sea.js's click handler no-ops
when no back-img sibling exists. Multi-user gameroom is a
known limitation here — the back-img src is the room
viewer's deck-back, not the drawing gamer's, which is wrong
when different gamers' decks have different backs. Parked
for a future multi-user polish pass.
{% endcomment %}
{% if request.user.is_authenticated and request.user.equipped_deck.has_card_images and not request.user.equipped_deck.is_polarized %}
<img class="sig-stage-card-back-img" alt=""
src="{{ request.user.equipped_deck.back_image_url }}">
{% endif %}
<button class="btn btn-reveal sea-stage-flip-btn" type="button" aria-label="Flip card">FLIP</button>
</div> </div>
<div class="sig-stat-block sea-stat-block"> <div class="sig-stat-block sea-stat-block">
<button class="btn btn-reverse spin-btn" type="button">SPIN</button> <button class="btn btn-reverse spin-btn" type="button">SPIN</button>