From 1963ad4c71a03769a029507bd321b8157d5459bc Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 25 May 2026 01:31:42 -0400 Subject: [PATCH] =?UTF-8?q?A.5-polish=20FLIP-to-back=20for=20non-polarized?= =?UTF-8?q?=20image-equipped=20decks=20=E2=80=94=20TDD.=20User-spec'd=20fe?= =?UTF-8?q?ature=202026-05-25=20PM=20after=20browser-verifying=20A.5:=20th?= =?UTF-8?q?e=20FLIP=20button=20on=20my=5Fsign.html=20cycles=20polarity=20f?= =?UTF-8?q?or=20polarized=20decks=20(Earthman)=20=E2=80=94=20gravity/levit?= =?UTF-8?q?y=20swap=20w.=20a=203D-spin=20animation,=20stat=20block=20updat?= =?UTF-8?q?es=20to=20the=20new=20polarity's=20emanation/reversal=20qualifi?= =?UTF-8?q?ers.=20For=20non-polarized=20decks=20(Minchiate=20today,=20futu?= =?UTF-8?q?re=20RWS-with-images,=20future=20classic-playing=20decks),=20po?= =?UTF-8?q?larity=20has=20no=20meaning=20=E2=80=94=20clicking=20FLIP=20jus?= =?UTF-8?q?t=20runs=20an=20animation=20that=20doesn't=20change=20anything?= =?UTF-8?q?=20content-wise.=20User=20wants=20FLIP=20repurposed=20for=20non?= =?UTF-8?q?-polarized=20decks:=20reveal=20the=20card-back=20image=20while?= =?UTF-8?q?=20leaving=20the=20stat=20block=20untouched,=20so=20the=20gestu?= =?UTF-8?q?re=20has=20visible=20payoff=20w/o=20forcing=20a=20meaningless?= =?UTF-8?q?=20polarity-state=20change.=20Implementation=20thread:=20server?= =?UTF-8?q?-side=20page=20wrapper=20carries=20a=20new=20`data-deck-polariz?= =?UTF-8?q?ed=3D"{{=20user.equipped=5Fdeck.is=5Fpolarized|yesno:'true,fals?= =?UTF-8?q?e'=20}}"`=20attr=20so=20the=20in-page=20JS=20can=20branch=20on?= =?UTF-8?q?=20it=20without=20making=20an=20API=20call=20or=20guessing=20fr?= =?UTF-8?q?om=20card=20data;=20stage-card=20scaffold=20conditionally=20ren?= =?UTF-8?q?ders=20a=20hidden=20``=20element?= =?UTF-8?q?=20when=20`equipped=5Fdeck.has=5Fcard=5Fimages=20AND=20NOT=20is?= =?UTF-8?q?=5Fpolarized`=20(image-equipped=20polarized=20decks=20would=20s?= =?UTF-8?q?till=20cycle=20polarity=20per=20existing=20flow=20=E2=80=94=20b?= =?UTF-8?q?ack-image=20element=20absent=20for=20them,=20no=20resource=20wa?= =?UTF-8?q?ste).=20JS=20branch=20in=20`flipBtn.click`:=20`if=20(pageEl.dat?= =?UTF-8?q?aset.deckPolarized=20=3D=3D=3D=20'false')=20{=20stageCard.class?= =?UTF-8?q?List.toggle('is-flipped-to-back')=20}=20else=20{=20=5FflipPolar?= =?UTF-8?q?ityAnimated()=20}`=20=E2=80=94=20same=20`.is-reversed`=20class?= =?UTF-8?q?=20toggle=20on=20the=20btn=20itself=20so=20visual=20feedback=20?= =?UTF-8?q?is=20consistent=20across=20both=20modes=20(btn=20rotates=20to?= =?UTF-8?q?=20signal=20"flipped=20state=20on").=20SCSS:=20`.sig-stage-card?= =?UTF-8?q?-back-img`=20joins=20the=20existing=20`.sig-stage-card-img`=20f?= =?UTF-8?q?ilter=20chain=20(same=20contour=20stroke=20+=20silhouette=20bla?= =?UTF-8?q?ck=20shadow=20=E2=80=94=20back=20image=20gets=20identical=20vis?= =?UTF-8?q?ual=20treatment=20to=20the=20front=20so=20the=20flip=20reads=20?= =?UTF-8?q?as=20same-deck=20consistency);=20default=20`display:=20none`;?= =?UTF-8?q?=20`.sig-stage-card.is-flipped-to-back`=20flips=20visibility=20?= =?UTF-8?q?=E2=80=94=20hides=20front,=20shows=20back.=20Stat=20block=20+?= =?UTF-8?q?=20arcana-key=20stroke=20color=20stay=20put=20per=20user=20spec?= =?UTF-8?q?=20=E2=80=94=20FLIP=20for=20non-polarized=20is=20purely=20a=20v?= =?UTF-8?q?isual=20reveal,=20no=20polarity-cycle=20or=20content=20swap.=20?= =?UTF-8?q?3=20new=20ITs=20in=20`MySignViewTest`:=20data-deck-polarized=3D?= =?UTF-8?q?"true"=20for=20default=20Earthman;=20data-deck-polarized=3D"fal?= =?UTF-8?q?se"=20+=20back-img=20element=20present=20w.=20correct=20v2-conv?= =?UTF-8?q?ention=20back=20asset=20URL=20when=20user=20switches=20to=20Min?= =?UTF-8?q?chiate;=20polarized=20deck=20omits=20the=20back-img=20element.?= =?UTF-8?q?=20No=20JS=20unit=20test=20(Jasmine=20spec)=20for=20the=20flipB?= =?UTF-8?q?tn=20branch=20=E2=80=94=20visual=20verify=20covers=20the=20hove?= =?UTF-8?q?r/click=20interaction;=20the=20IT=20covers=20the=20server-side?= =?UTF-8?q?=20conditional=20render=20that=20determines=20whether=20the=20b?= =?UTF-8?q?ranch=20can=20fire.=20No=20FT=20(the=20existing=20my=5Fsign=20F?= =?UTF-8?q?Ts=20cover=20the=20polarized-flip=20flow=20already;=20non-polar?= =?UTF-8?q?ized-flip=20is=20a=20CSS=20class=20toggle,=20low-risk=20for=20r?= =?UTF-8?q?egression).=20Tests:=203=20new=20green;=209/9=20MySignViewTest?= =?UTF-8?q?=20class=20green;=201303/1303=20IT+UT=20total=20green=20(71s;?= =?UTF-8?q?=20+3=20from=2082813e9's=201300).=20Out=20of=20scope:=20my=5Fse?= =?UTF-8?q?a's=20central=20sig=20card=20doesn't=20have=20a=20FLIP=20btn=20?= =?UTF-8?q?(no=20analogous=20behavior=20to=20add=20there);=20room.html=20F?= =?UTF-8?q?LIP=20behavior=20will=20be=20covered=20in=20A.8=20if=20applicab?= =?UTF-8?q?le;=20Sea=20Stage=20modal=20FLIP=20behavior=20(if=20any)=20land?= =?UTF-8?q?s=20in=20the=20my-sea=20fetch-endpoint=20extension=20later=20in?= =?UTF-8?q?=20A.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../billboard/tests/integrated/test_views.py | 43 +++++++++++++++++++ src/static_src/scss/_card-deck.scss | 15 ++++++- src/templates/apps/billboard/my_sign.html | 26 ++++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/apps/billboard/tests/integrated/test_views.py b/src/apps/billboard/tests/integrated/test_views.py index 73d3ccb..7ac59c3 100644 --- a/src/apps/billboard/tests/integrated/test_views.py +++ b/src/apps/billboard/tests/integrated/test_views.py @@ -860,6 +860,49 @@ class MySignViewTest(TestCase): {"card_id": 999999, "reversed": "0"}, ) self.assertEqual(response.status_code, 403) + + def test_page_carries_data_deck_polarized_attr(self): + """Sprint A.5-polish — the my_sign page wrapper exposes the equipped + deck's `is_polarized` state via `data-deck-polarized` so the FLIP-btn + JS can branch: polarized decks cycle polarity (existing behavior); + non-polarized decks flip to the deck card-back (new).""" + import lxml.html + # Default Earthman = is_polarized=True per A.0 migration. + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + [page] = parsed.cssselect(".my-sign-page") + self.assertEqual(page.get("data-deck-polarized"), "true") + + def test_image_deck_renders_back_img_in_stage_scaffold(self): + """Image-equipped non-polarized decks (Minchiate) render a hidden + inside the stage card; toggled visible + by the FLIP-btn JS handler via the .is-flipped-to-back class.""" + from apps.epic.models import DeckVariant + import lxml.html + minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + self.user.unlocked_decks.add(minchiate) + self.user.equipped_deck = minchiate + self.user.save(update_fields=["equipped_deck"]) + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + [page] = parsed.cssselect(".my-sign-page") + self.assertEqual(page.get("data-deck-polarized"), "false") + [back_img] = parsed.cssselect(".sig-stage-card .sig-stage-card-back-img") + self.assertIn( + "minchiate-fiorentine-1860-1890-back.png", + back_img.get("src", ""), + ) + + def test_polarized_deck_omits_back_img(self): + """Earthman (polarized) keeps the existing polarity-cycle FLIP — no + back-image element needed in the scaffold.""" + import lxml.html + response = self.client.get(reverse("billboard:my_sign")) + parsed = lxml.html.fromstring(response.content) + self.assertEqual( + len(parsed.cssselect(".sig-stage-card .sig-stage-card-back-img")), 0, + "Polarized deck must not render the back-image element", + ) self.user.refresh_from_db() self.assertIsNone(self.user.significator_id) diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 8ed3be7..e1a021e 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -681,7 +681,8 @@ html:has(.sig-backdrop) { display: none; } - .sig-stage-card-img { + .sig-stage-card-img, + .sig-stage-card-back-img { display: block; width: 100%; height: 100%; @@ -707,6 +708,18 @@ html:has(.sig-backdrop) { drop-shadow( 0 -0.2rem 0 var(--img-stroke-color)) drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1)); } + .sig-stage-card-back-img { display: none; } // shown only when flipped + + // Sprint A.5 — FLIP-to-back behavior for non-polarized image-equipped + // decks (Minchiate today). When `.is-flipped-to-back` is toggled by + // my_sign's flip-btn handler, the front face img hides + the deck + // card-back img shows. Stat block + arcana-key stroke color stay put — + // FLIP is purely a visual reveal of the card's back, no polarity-cycle + // or content swap. User spec 2026-05-25 PM. + &.is-flipped-to-back { + .sig-stage-card-img { display: none; } + .sig-stage-card-back-img { display: block; } + } } // ─── My Sign picker — sizing + state-gated reveal ──────────────────────────── diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 4787ebf..1e14e01 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -19,6 +19,7 @@ data-save-url="{% url 'billboard:save_sign' %}" {% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %} data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}" + data-deck-polarized="{{ request.user.equipped_deck.is_polarized|yesno:'true,false' }}" data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}"> {# Stage frame — always reserved at the top of the page; SAVE SIGN + #} @@ -35,6 +36,16 @@ {# DeckVariant.has_card_images). Hidden by default; CSS shows it #} {# when .sig-stage-card carries .sig-stage-card--image. #} + {# Sprint A.5 — for non-polarized image-equipped decks (Minchiate, #} + {# future RWS-with-images), the FLIP btn flips the card to its #} + {# back instead of cycling polarity (which has no meaning for #} + {# non-polarized decks). Pre-rendered back-image element; CSS #} + {# toggles visibility via `.sig-stage-card.is-flipped-to-back`. #} + {% if request.user.equipped_deck.has_card_images and not request.user.equipped_deck.is_polarized %} + + {% endif %}
@@ -351,7 +362,20 @@ if (flipBtn) { flipBtn.addEventListener('click', function () { if (!_currentCard) return; - _flipPolarityAnimated(); + // Sprint A.5 — non-polarized decks (Minchiate, RWS): FLIP + // shows the deck card-back instead of cycling polarity (no + // gravity/levity to toggle on these decks). Stat block stays + // unchanged — user spec 2026-05-25 PM. Polarized decks + // (Earthman) keep the existing polarity-flip animation. + if (pageEl.dataset.deckPolarized === 'false') { + stageCard.classList.toggle('is-flipped-to-back'); + if (flipBtn) flipBtn.classList.toggle( + 'is-reversed', + stageCard.classList.contains('is-flipped-to-back') + ); + } else { + _flipPolarityAnimated(); + } }); } if (spinBtn) {