A.7.5-polish-5 DRY _stat_face.html partial + FLIP-btn SCSS unification + my_sign FLIP DOM move-into-card + universal hover-reveal + instant mid-flip vanish + sea-stage-card image-mode bg fix + multi-line comment syntax cleanup. User-spec 2026-05-25 PM bundle of 5 cleanup threads atop polish-4 (4554c71).
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

**(1) `_stat_face.html` partial** — extracted to `templates/core/_partials/_stat_face.html` per user 2026-05-25 PM: "Why are there so many individual instances of this feature? Couldn't we call the same DRY partial for each?". One partial covers all 4 stat-block surfaces (sig-stat-block / sea-stat-block / fan-stage-block / my-sign-applet-stat-block) — ~80 lines of duplicated markup collapse to 7 `{% include %}` sites (3 surfaces × 2 faces + applet × 1 face). Args: `face_modifier` (required: "upright"|"reversed"), `label_text` (required: "Emanation"|"Reversal"), `card` (optional TarotCard for applet's server-render path), `keywords_ul_id` (optional id attr on the keyword `<ul>` — sea_stage + fan need `id_sea_stat_upright/reversed` + `id_fan_stat_upright/reversed` for stage-card.js's `populateKeywords` surface-specific selector overrides). The `.stat-face` wrapper that the partial introduces is a no-op for the applet — applet's bespoke `.my-sign-applet-stat-block` rule doesn't `@include stat-block-shared` so `.stat-face` inherits no padding / display-none from the shared mixin.

**(2) FLIP-btn `@mixin flip-btn-base` + `%flip-btn-revealed` + `%flip-btn-mid-flip` primitives** — `_card-deck.scss` head per user 2026-05-25 PM: "unify the many disparate calculations we use for when we allow that FLIP btn to appear and where it appears". Each surface's flip-btn declaration now `@include`s the base (position absolute + zero margin + hidden default opacity 0 + 0.3s transition) and `@extend`s `%flip-btn-revealed` on its surface-specific reveal trigger + `%flip-btn-mid-flip` on its surface-specific `[data-flipping]` selector chain. ~30 lines of duplication collapsed to 6 lines of mixin/placeholder + 3 `@include` + 4 `@extend` calls.

**(3) my_sign FLIP btn moved INSIDE `.sig-stage-card`** + `.my-sign-flip-btn` + `.my-sign-applet-flip-btn` share one positioning rule (`bottom: 0.6rem; left: 0.6rem`) — was a sibling under `.my-sign-stage` positioned via stage-padding-relative `calc(1.5rem + 0.4rem)`. Polish-5 nests it INSIDE the card so positioning is naturally card-relative + the separate `.my-sign-page[data-current-card-id]` centered-mode geometric override (re-deriving offsets from the centred-row layout) is DROPPED entirely. The applet was already inside-card positioned; same `bottom: 0.6rem; left: 0.6rem` rule combines both surfaces in a single `_card-deck.scss` declaration. The applet's `_billboard.scss` flip-btn rule is now just a shim `@include` + `@extend` (the positioning got DRY'd up to the shared rule).

**(4) Hover-reveal everywhere** + instant mid-flip vanish — user-spec 2026-05-25 PM: "The .btn-reveal behavior here should now (1) disappear much earlier, so no independent ease-in/-out logic needed on clicking FLIP; (2) calculate its position more dynamically; be mirrored in the gameboard's My Sign applet. In all places does the hover-to-reveal-FLIP-.btn-reveal effect abate while the card is finishing a FLIP". my_sign main flipped from `display: none → display: inline-flex` (frozen-gated) to opacity-based hover-reveal on `.sig-stage-card:hover` (still gated by `.sig-stage--frozen`). Applet flipped from always-visible to opacity-based hover-reveal on `.my-sign-applet-card:hover`. Fan kept its existing hover-reveal. Mid-flip-hide changed from `opacity: 0 + pointer-events: none` (faded out over the 0.3s transition, which competed w. the click) to `display: none` — INSTANT vanish, no ease-out animation. All 3 surfaces consolidated into one combined `[data-flipping] -> flip-btn` selector list extending `%flip-btn-mid-flip`. The `:has(.flip-btn:hover)` self-pin clause (already present on fan) added to my_sign + applet too — keeps the btn visible while the cursor is on it, otherwise the btn (z-index 25, on top of the card) steals `:hover` from the card the moment the cursor moves onto it + retracts the reveal mid-click.

**(5) `.sea-stage--levity .sea-stage-card` image-mode bg fix** — user-reported 2026-05-25 PM: "the card preview stage in my_sea.html still sports the old card bg (the --secUser here) behind the card img (with the --quiUser box-shadow border)". Same source-order collision pattern as the sea-sig-card fix in polish-4: `.sea-stage--levity .sea-stage-card`'s `@include stage-card-polarity($invert-frame: true)` sets `background: rgba(var(--secUser), 1) + border-color: rgba(var(--priUser), 1)` at specificity 0,2,0 — matches the shared `.sig-stage-card.sig-stage-card--image` comma-list rule's specificity but source-loses to it (levity rule lives at line 2150, comma-list at line 705). Fix: add a `&.sig-stage-card--image { background: transparent; border: 0; }` nested override (0,3,0 specificity) — re-states the transparency under the levity polarity branch so image-mode drawn cards (Minchiate today) don't show a beige card-shape behind the PNG art. The gravity branch was already fine (its mixin call doesn't pass `$invert-frame`).

**(6) Multi-line `{# #}` comment syntax cleanup** — user-spotted 2026-05-25 PM after my polish-5 partial extraction caused visible comment text to leak into rendered HTML on 4 templates (per [[feedback-django-multiline-comments]] / [[feedback-django-comments-single-line-only]] traps the user has flagged before). All multi-line block comments I added in this polish converted to `{% comment %}...{% endcomment %}` form — covers the `_stat_face.html` partial header + 4 template include sites (my_sign.html × 2 blocks, _applet-my-sign.html, _sea_stage.html, game_kit.html).

Tests: 1314/1314 IT+UT total green (72s). No new tests — existing chip-presence + image-mode ITs from polish-4 still pass through the partial extraction. Visual verify 2026-05-25 PM via Claudezilla: my_sign main page (Queen of Coins) renders cleanly via partial w. card+stat-block; applet renders cleanly w. server-filled chip + title; carousel + sea_stage modal work via JS-populated partial includes; my_sign FLIP btn moved into card + hover-reveals + vanishes instantly on FLIP click; sea-stage-card no longer shows --secUser bg behind image-mode PNG art under levity. DRY partial extraction was held out of polish-4 as user-requested separate concern: "hold it for a separate commit, but fold the FLIP btn unification into it as the styling cleanup part" — done.

**Follow-up parked for next sprint**: user-flagged 2026-05-25 PM "If it's interfering to have bespoke rules, just allow the FLIP btn everywhere, including in my_sea.html". This needs (a) dropping the `not card.deck_variant.is_polarized` server-render gate in the applet template, (b) adding a FLIP btn + back-img element to the `_sea_stage.html` modal scaffold, (c) wiring a JS handler in sea.js (currently has no FLIP behavior for drawn-card stage). Out of scope for the polish-5 commit since it's template + JS scope; will pick up as polish-6 or a fresh sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 19:22:08 -04:00
parent a03d0b0cac
commit 1e2041ed9f
7 changed files with 206 additions and 185 deletions

View File

@@ -74,24 +74,13 @@
{# only the polarity axis (FLIP), never the orientation axis #}
{# (SPIN), so always render the upright/emanation face. #}
<div class="my-sign-applet-stat-block" data-arcana-key="{{ card.arcana }}">
{# Sprint A.7.5 — `.stat-face-header` wraps the rank+suit chip #}
{# inline w. EMANATION per [[project-image-based-deck-face- #}
{# rendering]]'s A.3 Q3 spec. Server-rendered (read-only #}
{# applet — no JS populate path). #}
<div class="stat-face-header">
<span class="stat-chip-rank">{{ card.corner_rank }}</span>
<div class="stat-chip-tag">
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }} stat-chip-icon"></i>{% endif %}
<p class="stat-face-label">Emanation</p>
</div>
</div>
<p class="stat-face-title">{{ card.name }}</p>
<p class="stat-face-arcana">{{ card.get_arcana_display }}</p>
<ul class="stat-keywords">
{% for kw in card.keywords_upright %}
<li>{{ kw }}</li>
{% endfor %}
</ul>
{% comment %}
DRY stat-face — see `core/_partials/_stat_face.html`. The
applet is the only server-render consumer (no SPIN, single
emanation face); passes `card` so chip + title + arcana +
keywords are filled from `card.*` at render time.
{% endcomment %}
{% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" card=card %}
</div>
{# Sprint A.6 — applet FLIP btn handler. Mirrors my_sign.html's #}
{# `_flipToBackAnimated()` shape (rotateY 0→90→0 over 500ms, class #}

View File

@@ -5,15 +5,17 @@
{% block header_text %}<span>Game</span><span>Sign</span>{% endblock header_text %}
{% block content %}
{# Two-phase picker. Landing renders the DRY table hex (1-chair) w. a #}
{# central SCAN SIGN btn; the stage frame above previews the user's #}
{# saved sig if any. Clicking SCAN SIGN swaps to picker phase: the hex #}
{# hides, the card grid appears below the stage. Selection is a two-step #}
{# click on the thumbnail itself (matching room sig-select): click thumb #}
{# → OK btn appears; click OK → lock (stat block + FLIP + SAVE SIGN #}
{# enable + NVM appears for deselect). #}
{# "Significator" is preserved at the storage layer (User.significator); #}
{# this billboard surface re-brands to "Sign". #}
{% comment %}
Two-phase picker. Landing renders the DRY table hex (1-chair) w. a
central SCAN SIGN btn; the stage frame above previews the user's
saved sig if any. Clicking SCAN SIGN swaps to picker phase: the hex
hides, the card grid appears below the stage. Selection is a two-step
click on the thumbnail itself (matching room sig-select): click thumb
→ OK btn appears; click OK → lock (stat block + FLIP + SAVE SIGN
enable + NVM appears for deselect).
"Significator" is preserved at the storage layer (User.significator);
this billboard surface re-brands to "Sign".
{% endcomment %}
<div class="my-sign-page"
data-phase="landing"
data-save-url="{% url 'billboard:save_sign' %}"
@@ -69,10 +71,19 @@
<span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i>
</div>
{% comment %}
Sprint A.7.5-polish-5 — FLIP btn moved INSIDE .sig-stage-card
(was sibling under .my-sign-stage). Positioning is now relative
to the card itself: card-bottom-left via `bottom: 0.6rem;
left: 0.6rem;` (shared w. the applet + future fan move per
`.my-sign-flip-btn, .my-sign-applet-flip-btn` combined rule in
`_card-deck.scss`). Drops the prior stage-relative calc + the
centered-mode geometric override. Hover-reveal on the card
itself (`:hover` + `:has(.my-sign-flip-btn:hover)`) matches
the fan carousel's progressive-disclosure pattern.
{% endcomment %}
<button class="btn btn-reveal my-sign-flip-btn" type="button">FLIP</button>
</div>
{# FLIP — bottom-left of the stage card. Visible only after lock #}
{# (.sig-stage--frozen). #}
<button class="btn btn-reveal my-sign-flip-btn" type="button">FLIP</button>
<div class="sig-stat-block">
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
<button class="btn btn-info fyi-btn" type="button">FYI</button>
@@ -83,35 +94,15 @@
{# `.stat-face-title` + `.stat-face-arcana` empty by default, #}
{# populated by stage-card.js `populateStatBlock` from the card #}
{# data flow on focus / save. #}
{# Sprint A.7.5 — `.stat-face-header` wraps the new top-left #}
{# rank+suit chip inline w. the underlined EMANATION/REVERSAL #}
{# label per [[project-image-based-deck-face-rendering]]'s A.3 #}
{# Q3 spec. Chip elements empty by default; stage-card.js's #}
{# populateStatExtras fills `.stat-chip-rank` + `.stat-chip-icon`.#}
<div class="stat-face stat-face--upright">
<div class="stat-face-header">
<span class="stat-chip-rank"></span>
<div class="stat-chip-tag">
<i class="fa-solid stat-chip-icon" style="display:none"></i>
<p class="stat-face-label">Emanation</p>
</div>
</div>
<p class="stat-face-title"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords"></ul>
</div>
<div class="stat-face stat-face--reversed">
<div class="stat-face-header">
<span class="stat-chip-rank"></span>
<div class="stat-chip-tag">
<i class="fa-solid stat-chip-icon" style="display:none"></i>
<p class="stat-face-label">Reversal</p>
</div>
</div>
<p class="stat-face-title"></p>
<p class="stat-face-arcana"></p>
<ul class="stat-keywords"></ul>
</div>
{% comment %}
DRY stat-face — see `core/_partials/_stat_face.html` for the
header (chip + EMANATION/REVERSAL label) + title + arcana +
keywords structure. JS-populated surface; no `card` arg, no
`keywords_ul_id` arg — stage-card.js's populateStatExtras +
populateKeywords fill the empty placeholders at runtime.
{% endcomment %}
{% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" %}
{% include "core/_partials/_stat_face.html" with face_modifier="reversed" label_text="Reversal" %}
{% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_my_sign_fyi_panel" %}
</div>
{# SAVE SIGN + NVM form lives inside the stage so the layout #}