diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index d4e181b..6416f73 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -582,21 +582,21 @@ body.page-billposts { } // Sprint A.6 — FLIP btn for non-polarized image-equipped decks in the - // applet. Nested INSIDE the .my-sign-applet-card.--image (which has - // position: relative) so absolute positioning anchors to the card bounds. - // Hidden during the rotateY animation via the [data-flipping] hook on - // the parent card — same pattern as the my_sign page (`_card-deck.scss:889`) - // and the tarot-fan view (`_card-deck.scss:459`). + // applet. Nested INSIDE .my-sign-applet-card.--image (which has + // position: relative) so absolute positioning anchors to the card. + // Polish-5: shares `@include flip-btn-base` + `%flip-btn-mid-flip` w. + // the other 2 surfaces (my_sign main + game-kit fan) via `_card-deck.scss`. .my-sign-applet-card .my-sign-applet-flip-btn { - position: absolute; + @include flip-btn-base; z-index: 10; bottom: 0.6rem; left: 0.6rem; - margin: 0; } + // Btn is nested INSIDE the card, so the [data-flipping] hook is a direct + // ancestor — no `:has()` needed (unlike the my_sign + fan surfaces where + // the btn is a sibling of the flipping card under a common parent). .my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn { - opacity: 0; - pointer-events: none; + @extend %flip-btn-mid-flip; } // Stat block — mirrors the stage card's footprint (same 5:8 aspect + diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 8ef0614..117b219 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -3,6 +3,47 @@ // Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav) // extracted from _game-kit.scss; sig-select overlay extracted from _room.scss. +// ── Shared FLIP-btn primitives ─────────────────────────────────────────────── +// +// The FLIP btn lives on 3 surfaces (my_sign main stage, my_sign applet, game +// kit fan). Polish-5 2026-05-25 PM unification per user spec: hover-reveal +// everywhere (was display-toggle on my_sign + always-visible on applet); +// `display: none` mid-flip for INSTANT vanish (was opacity-fade — felt sluggish +// since clicks happen faster than the 0.3s ease). The reveal-on-hover transition +// stays smooth for everyday hover, but the moment a FLIP starts the btn pops +// out of layout entirely so it can't visually interfere w. the rotateY mid-spin. +// +// @mixin flip-btn-base Position absolute + zero margin + hidden default +// (opacity:0 + pointer-events:none) + 0.3s opacity +// transition for the smooth hover-reveal. Surfaces +// add z-index + position offsets (most use bottom- +// left-of-card; fan uses transform-based carousel- +// shift since its btn is at wrap-level). +// %flip-btn-revealed opacity:1 + pointer-events:auto — applied by +// each surface's hover-trigger selector chain. +// %flip-btn-mid-flip display:none — applied by each surface's +// `[data-flipping]` selector chain. Instant vanish +// (display isn't animatable); supersedes the +// hover-reveal entirely while rotation is in +// flight. Each surface's selector chain differs +// because the btn-to-card DOM relationship varies +// (inside the card on my_sign + applet post-polish- +// 5; sibling under common wrap for the fan). +@mixin flip-btn-base { + position: absolute; + margin: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; +} +%flip-btn-revealed { + opacity: 1; + pointer-events: auto; +} +%flip-btn-mid-flip { + display: none; +} + // ── Shared stage-card polarity rules ───────────────────────────────────────── // // Used by .sea-stage-card (Sea Select polarity is fixed by the deck-stack the @@ -519,34 +560,32 @@ // Positioned at bottom-left of the focused card slot (carousel-shifted, so its // translateX matches .tarot-fan's leftward shift). .fan-flip-btn { - position: absolute; + @include flip-btn-base; z-index: 25; top: 50%; left: 50%; + // Carousel-shifted bottom-left-of-focused-card. The btn sits at wrap level + // (not inside any card) so positioning has to derive from the carousel + // geometry vars rather than the bottom-left-of-card pattern the other 2 + // surfaces use. --fan-stage-shift + --fan-card-w/h scale w. breakpoint + // via the parent `.tarot-fan-wrap`. transform: translate(calc(-50% - var(--fan-stage-shift) - var(--fan-card-w) / 2 + 1.5rem), calc(-50% + var(--fan-card-h) / 2 - 1.5rem)); - margin: 0; - opacity: 0; - pointer-events: none; - transition: opacity 0.3s ease; } -// Reveal when the focused card OR the FLIP button itself is hovered. Without -// the `.fan-flip-btn:hover` clause the button (z-index 25, sitting on top of -// the card) steals :hover from the card the moment the cursor moves onto it, -// flipping :has() false, fading the button to opacity:0 + pointer-events:none, -// and letting the in-flight click pass through to the dialog backdrop (which -// closes the modal). Keeping the button in the trigger list pins it visible -// while the cursor is on it. +// Hover-reveal. The `.fan-flip-btn:hover` clause pins the btn visible while +// the cursor is on it — without it, the btn (z-index 25, on top of the card) +// steals :hover from the card the moment the cursor moves onto it, retracting +// the reveal + letting the in-flight click pass through to the dialog backdrop +// (which closes the modal). `.fan-touch-revealed` is the touch-device fallback +// (no :hover on touchscreens) — game-kit.js toggles it on first card tap. .tarot-fan-wrap:has(.fan-card--active:hover) .fan-flip-btn, .tarot-fan-wrap:has(.fan-flip-btn:hover) .fan-flip-btn, .tarot-fan-wrap.fan-touch-revealed .fan-flip-btn { - opacity: 1; - pointer-events: auto; -} -.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn { - opacity: 0; - pointer-events: none; + @extend %flip-btn-revealed; } +// Mid-flip-hide selector for the fan is consolidated into the unified +// 3-surface rule below the `.my-sign-flip-btn` / `.my-sign-applet-flip-btn` +// declarations. See `_card-deck.scss` polish-5 FLIP-btn block. .fan-nav { position: absolute; @@ -867,23 +906,10 @@ html:has(.sig-backdrop) { // pair aligned in the centred row. .sig-stat-block { align-self: center; } - // FLIP was positioned via `left: calc(1.5rem + 0.4rem)` (default - // rule below) assuming the card sat flush against the stage's - // padded-left edge — true on the picker's left-anchored layout but - // wrong here w. `justify-content: center` (the card moves to - // wherever the group's left edge lands). - // Re-derive FLIP's offsets from the centred geometry: - // group width = card + gap + stat = 2 * --sig-card-w + 0.75rem - // card's left edge (in stage) = (100% - group width) / 2 - // card's bottom edge (in stage) = 50% - (cardHeight / 2) - // = 50% - --sig-card-w * 0.8 - // (cardHeight = w × 8/5 = w × 1.6) - // The +0.4rem on each lands FLIP just inside the card's bottom-left - // corner, matching the picker-side positioning intent. - .my-sign-flip-btn { - left: calc((100% - 2 * var(--sig-card-w) - 0.75rem) / 2 + 0.4rem); - bottom: calc(50% - var(--sig-card-w) * 0.8 + 0.4rem); - } + // Polish-5: FLIP-btn centered-mode offset override DROPPED. The btn is + // now positioned INSIDE the card (`.sig-stage-card { position: relative }` + // + btn `bottom: 0.6rem; left: 0.6rem`), so it follows the card naturally + // wherever the stage positions it — no per-layout-mode geometric calc. // Landing collapses since the hex is server-side gone — just DEL is // left + that's `position: absolute`. `position: static` here drops @@ -963,34 +989,44 @@ html:has(.sig-backdrop) { background: rgba(var(--duoUser), 1); } -.my-sign-flip-btn { - position: absolute; +// Polish-5: my_sign main + applet FLIP btns share one positioning rule. +// Both live INSIDE their card (`.sig-stage-card` / `.my-sign-applet-card`, +// both `position: relative`) so `bottom: 0.6rem; left: 0.6rem` anchors +// universally to card-bottom-left w/o needing surface-specific calc(). +.my-sign-flip-btn, +.my-sign-applet-flip-btn { + @include flip-btn-base; z-index: 25; - bottom: 0.4rem; - // .sig-stage has padding-left: 1.5rem; this offset places the btn just - // inside the stage card's bottom-left corner (the card sits flex-end / - // flex-start, anchored to the stage's left padding). - left: calc(1.5rem + 0.4rem); - margin: 0; - display: none; + bottom: 0.6rem; + left: 0.6rem; } -// FLIP btn appears only when the stage is frozen (post-OK confirm). Hover-only -// previews don't reveal the polarity toggle — the user hasn't committed yet. -.my-sign-stage.sig-stage--frozen .my-sign-flip-btn { - display: inline-flex; +// Hover-reveal on the parent card. `:has(.flip-btn:hover)` pins the btn +// visible while the cursor is on it — without this clause, the btn (z-index +// 25, on top of the card) steals :hover from the card the moment the cursor +// moves onto it, retracting the reveal + breaking the click flow. Same +// pattern the fan carousel uses. +// +// my_sign main still gates on `.sig-stage--frozen` — hover-only previews +// don't reveal the polarity toggle until the user has committed a sig. +.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-applet-card:hover .my-sign-applet-flip-btn, +.my-sign-applet-card:has(.my-sign-applet-flip-btn:hover) .my-sign-applet-flip-btn { + @extend %flip-btn-revealed; } -// Sprint A.5 — hide FLIP btn during the flip animation. `data-flipping="1"` -// is set on .sig-stage-card by _flipPolarityAnimated (polarized) AND -// _flipToBackAnimated (non-polarized) for the 500ms animation duration; CSS -// :has() selects the parent .my-sign-stage when any child carries that attr -// and zeros the btn so it doesn't visually interfere w. the rotateY mid-spin. -// Mirrors the tarot-fan view's pattern (`_card-deck.scss:459` — -// `.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn`). -.my-sign-stage:has(.sig-stage-card[data-flipping]) .my-sign-flip-btn { - opacity: 0; - pointer-events: none; +// Unified mid-flip-hide across all 3 surfaces. `[data-flipping]="1"` is set on +// 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 +// 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 +// INSIDE the card on my_sign + applet post-polish-5; sibling under .tarot- +// fan-wrap for the fan). +.sig-stage-card[data-flipping] .my-sign-flip-btn, +.my-sign-applet-card[data-flipping] .my-sign-applet-flip-btn, +.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn { + @extend %flip-btn-mid-flip; } // ─── Mini card grid ─────────────────────────────────────────────────────────── @@ -2120,6 +2156,17 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f color: rgba(var(--priUser), 1); .fan-card-arcana, .fan-card-corner { color: rgba(var(--priUser), 1); } + + // Polish-5: image-mode override mirrors the sea-sig-card pattern. The + // `$invert-frame: true` arg above sets `--secUser` bg + `--priUser` + // border, which then OUT-CASCADES the shared image-mode comma-list rule + // (same 0,2,0 specificity but later in source). Re-state transparency + // here so image-mode drawn cards (Minchiate today) don't show a beige + // card-shape behind the PNG art. + &.sig-stage-card--image { + background: transparent; + border: 0; + } } .sea-stage--gravity .sea-stage-card { @include stage-card-polarity( diff --git a/src/templates/apps/billboard/_partials/_applet-my-sign.html b/src/templates/apps/billboard/_partials/_applet-my-sign.html index 928c900..d7f98ad 100644 --- a/src/templates/apps/billboard/_partials/_applet-my-sign.html +++ b/src/templates/apps/billboard/_partials/_applet-my-sign.html @@ -74,24 +74,13 @@ {# only the polarity axis (FLIP), never the orientation axis #} {# (SPIN), so always render the upright/emanation face. #}
- {# 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). #} -
- {{ card.corner_rank }} -
- {% if card.suit_icon %}{% endif %} -

Emanation

-
-
-

{{ card.name }}

-

{{ card.get_arcana_display }}

- + {% 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 %}
{# Sprint A.6 — applet FLIP btn handler. Mirrors my_sign.html's #} {# `_flipToBackAnimated()` shape (rotateY 0→90→0 over 500ms, class #} diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 158742e..b6c30ce 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -5,15 +5,17 @@ {% block header_text %}GameSign{% 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 %}
+ {% 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 %} + - {# FLIP — bottom-left of the stage card. Visible only after lock #} - {# (.sig-stage--frozen). #} -
@@ -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`.#} -
-
- -
- -

Emanation

-
-
-

-

- -
-
-
- -
- -

Reversal

-
-
-

-

- -
+ {% 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" %}
{# SAVE SIGN + NVM form lives inside the stage so the layout #} diff --git a/src/templates/apps/gameboard/_partials/_sea_stage.html b/src/templates/apps/gameboard/_partials/_sea_stage.html index 17c9d23..56b3093 100644 --- a/src/templates/apps/gameboard/_partials/_sea_stage.html +++ b/src/templates/apps/gameboard/_partials/_sea_stage.html @@ -45,34 +45,13 @@
- {# Sprint A.7.5 — `.stat-face-header` wraps the rank+suit chip #} - {# inline w. EMANATION/REVERSAL per [[project-image-based-deck- #} - {# face-rendering]]'s A.3 Q3 spec. Chip empty by default; stage- #} - {# card.js populateStatExtras fills both faces' chips identically.#} -
-
- -
- -

Emanation

-
-
-

-

- -
-
-
- -
- -

Reversal

-
-
-

-

- -
+ {% comment %} + DRY stat-face — see `core/_partials/_stat_face.html`. JS- + populated; keyword ul carries id for stage-card.js's surface- + specific selector overrides (populateKeywords opts). + {% endcomment %} + {% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" keywords_ul_id="id_sea_stat_upright" %} + {% include "core/_partials/_stat_face.html" with face_modifier="reversed" label_text="Reversal" keywords_ul_id="id_sea_stat_reversed" %} {% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_sea_fyi_panel" panel_extra_attrs='style="display:none"' %}
diff --git a/src/templates/apps/gameboard/game_kit.html b/src/templates/apps/gameboard/game_kit.html index a26e6be..9d5ac1a 100644 --- a/src/templates/apps/gameboard/game_kit.html +++ b/src/templates/apps/gameboard/game_kit.html @@ -21,37 +21,14 @@
- {# Sprint A.7.5 — match the my_sign / sea_stage stat-block shape: #} - {# `.stat-face-header` w. chip + EMANATION/REVERSAL label, then #} - {# `.stat-face-title` + `.stat-face-arcana` (the carousel stat #} - {# block previously had only the keyword list; for image-mode #} - {# decks the stat block is the only home for textual metadata). #} - {# All four — chip rank, chip icon, title, arcana — populated by #} - {# stage-card.js's populateStatExtras on each card focus change. #} -
-
- -
- -

Emanation

-
-
-

-

- -
-
-
- -
- -

Reversal

-
-
-

-

- -
+ {% comment %} + DRY stat-face — see `core/_partials/_stat_face.html`. JS- + populated; keyword ul carries id for stage-card.js's + populateKeywords opts override (game-kit.js passes + uprightSel/reversedSel pointing at these IDs). + {% endcomment %} + {% include "core/_partials/_stat_face.html" with face_modifier="upright" label_text="Emanation" keywords_ul_id="id_fan_stat_upright" %} + {% include "core/_partials/_stat_face.html" with face_modifier="reversed" label_text="Reversal" keywords_ul_id="id_fan_stat_reversed" %} {% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_fan_fyi_panel" panel_extra_attrs='style="display:none"' %}
diff --git a/src/templates/core/_partials/_stat_face.html b/src/templates/core/_partials/_stat_face.html new file mode 100644 index 0000000..b3686f9 --- /dev/null +++ b/src/templates/core/_partials/_stat_face.html @@ -0,0 +1,38 @@ +{% comment %} +DRY stat-face partial — used by all 4 stat-block surfaces: +sig-stat-block (my_sign main + sig-overlay), sea-stat-block (sea_stage +modal), fan-stage-block (game_kit carousel), my-sign-applet-stat-block +(billboard applet). Sprint A.7.5-polish-5 DRY extraction 2026-05-25 PM +per user request "Why are there so many individual instances of this +feature? Couldn't we call the same DRY partial for each?". + +Args: + face_modifier Required. "upright" or "reversed" — appended to .stat-face + BEM modifier class. The 3 SPIN-capable surfaces (my_sign / + sea_stage / fan) call this partial TWICE (once per face) + so .is-reversed on the parent stat-block can swap which + face displays. The applet calls it ONCE w. "upright". + label_text Required. "Emanation" or "Reversal" — static label text. + card Optional. TarotCard instance for server-render mode (the + applet's sole consumption pattern). When present: chip + rank/icon + title + arcana + keywords are server-filled + from card.*; when None: fields render empty for stage- + card.js's `populateStatExtras` to populate at runtime + (sig / sea_stage / fan stage). + keywords_ul_id Optional. Adds `id="..."` to the keyword `