feat: My Sign saved-sig state — --duoUser bg, centred card+stat-block, stage card auto-rotates for reversed sigs on landing. Three follow-up polish items atop the f609313 read-only-saved-sig batch.
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

(1) **`--duoUser` bg on the saved-sig aperture.** Per user spec — once the table hex is server-side gone (f609313's `{% if not current_significator %}` wrap), the now-mostly-empty olive aperture reads as a distinct mode vs the default landing (--priUser bg w. hex). New `.my-sign-page[data-current-card-id] { background-color: rgba(var(--duoUser), 1); }` block in `_card-deck.scss:644-696`. Keyed on `data-current-card-id` (present only when `current_significator` is set per `my_sign.html:20`) rather than the absence of `[data-phase="landing"]` — picker also lacks the hex but should keep --priUser. Mirrors how `.my-sea-page[data-phase="picker"]` swaps bg in `_gameboard.scss`.

(2) **Stage card + stat block centre in the aperture.** Default landing left-anchored the stage natural-sized at the top of the column (above the hex which filled the rest); w. the hex gone there's a wide empty page bottom. `.my-sign-page[data-current-card-id] .my-sign-stage` overrides to `flex: 1; justify-content: center; align-items: center; padding-left: 0;` — stage grows to fill, card+stat-block centre as a unit. `.my-sign-landing` collapses to `flex: 0 0 auto` + `position: static`; DEL is `position: absolute` so it walks up to `.my-sign-page` (already `position: relative`) + pins to the page corner. **2 traps caught mid-build** in the centring pass: (a) `.sig-stat-block`'s default `align-self: flex-end` (`_card-deck.scss:599`) overrode the parent's `align-items: center` on the cross axis, so the stat block floated to the bottom of the stage while the card sat at vertical-centre — forced `align-self: center` on this state. (b) `.my-sign-flip-btn`'s `left: calc(1.5rem + 0.4rem)` (`_card-deck.scss:747`) assumed the card sat flush against `.sig-stage`'s padded-left edge — true on the picker but wrong w. `justify-content: center`, FLIP landed at the stage's left edge w. the card centred ~3rem to the right of it. Re-derived left/bottom from the centred geometry: card's left edge in stage = `(100% - 2 * sig-card-w - 0.75rem) / 2` (the centred card+gap+stat group's left), card's bottom edge = `50% - sig-card-w * 0.8` from stage bottom (cardHeight = sig-card-w × 8/5 = × 1.6, half = × 0.8). `+ 0.4rem` on each lands FLIP just inside the card's bottom-left corner, same offset as the picker-side intent.

(3) **Stage card auto-rotates 180° on landing for saved-reversed sigs.** Server-side `data-polarity` attribute on `.my-sign-page` already reflected `significator_reversed` correctly (drives the polarity-themed color rules at `_card-deck.scss:917-1042` for levity/gravity ink) but the visual 180° rotation lives in the `stage-card--reversed` class which was only JS-applied via `_toggleOrientation()` (SPIN btn handler). On init w. a saved sig, `_populateStage(savedCardEl)` filled the card's data but didn't touch rotation — so saved-reversed sigs rendered upright on landing while the My Sign applet (template-driven, reads `request.user.significator_reversed` directly + conditionally adds `stage-card--reversed` per `_applet-my-sign.html:9`) correctly rotated them. Two surfaces disagreed → user read the applet as inverted ("non-reversed sig displays upside-down in the applet"). Actually the my_sign.html stage was the liar; the applet was right. Fixed at `my_sign.html:404-406` — after `_populateStage(savedCardEl) + stage.classList.add('sig-stage--frozen')`, if `revInput.value === '1'` (= saved reversed=True) call `_toggleOrientation()` once. That helper covers all three coordinated state mutations: `stageCard.classList.toggle('stage-card--reversed', on)` (visual 180° rotation), `statBlock.classList.toggle('is-reversed', on)` (swaps to reversal face per `_card-deck.scss:62-65`), `spinBtn.classList.toggle('is-reversed', on)` (visual indicator). Both surfaces now agree. Per user direction, a follow-up will lock my_sign.html SAVE to always write `reversed=False` (Tarot-tradition convention) — but the underlying rotation pipeline still has to work for room-side sig-select where reversed sigs are needed.

**TDD coverage**: no new tests — `test_landing_previews_saved_sig_on_stage` (updated in f609313) still passes as written (its assertions are around the frozen-stage + stat-block-visible + hex-absent contract, all of which hold under the centring + rotation patches). The reversed-sig-auto-rotate case is light-weight enough (one branch w. a well-known helper) to not need a dedicated FT; if it regresses, the existing room-side `_toggleOrientation` coverage in the gameboard FTs catches the helper itself + manual verify caught it here. Manual verify done on /billboard/my-sign/ w. `disco`'s saved Jack of Brands (reversed=False, renders upright + centred w. --duoUser bg + FLIP on card's bottom-left + DEL on page's bottom-right). 1211 IT/UT still green; one minor visual to chase before locking my_sign.html to non-reversed-only — verifying that a reversed-saved sig renders rotated on return (DB has none currently, will test after the follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-22 13:21:55 -04:00
parent f6093136f1
commit 1452de1a76
2 changed files with 73 additions and 0 deletions

View File

@@ -636,6 +636,65 @@ html:has(.sig-backdrop) {
position: relative; position: relative;
} }
// Saved-sig read-only state — page bg shifts to --duoUser so the now-
// hexless aperture reads as a distinct mode (mirrors how `.my-sea-page
// [data-phase="picker"]` swaps bg in `_gameboard.scss`). Keyed on the
// presence of `data-current-card-id` since that attribute renders only
// when the user has a saved significator. Stage card + stat block also
// center in the now-empty page aperture (default landing keeps stage
// natural-sized at the top above the hex; here there's no hex so the
// stage gets to grow + middle itself).
.my-sign-page[data-current-card-id] {
background-color: rgba(var(--duoUser), 1);
// Stage grows to fill the available column space + centres its card
// row both horizontally + vertically. Override `.sig-stage`'s default
// `align-items: flex-end` + `padding-left: 1.5rem` so card + stat
// block land truly centred.
.my-sign-stage {
flex: 1;
min-height: 0;
justify-content: center;
align-items: center;
padding-left: 0;
}
// `.sig-stat-block`'s default `align-self: flex-end` (line 599)
// overrides the parent's `align-items: center` on the cross axis,
// so the stat block was floating to the bottom of the stage while
// the card sat at vertical-centre. Force `center` here to keep the
// 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);
}
// Landing collapses since the hex is server-side gone — just DEL is
// left + that's `position: absolute`. `position: static` here drops
// landing's positioning context so DEL walks up to `.my-sign-page`
// (already `position: relative`) + pins to the page corner.
.my-sign-landing {
flex: 0 0 auto;
min-height: 0;
position: static;
}
}
// Stage frame — fixed slice in picker phase, natural-sized on landing. // Stage frame — fixed slice in picker phase, natural-sized on landing.
// The picker min-height reserves real estate so hover-preview cards don't // The picker min-height reserves real estate so hover-preview cards don't
// shift adjacent layout; on landing the stage shrinks to its actual content // shift adjacent layout; on landing the stage shrinks to its actual content

View File

@@ -384,6 +384,17 @@
// hidden, DEL is the only action). The picker grid stays hidden // hidden, DEL is the only action). The picker grid stays hidden
// until SCAN SIGN — but SCAN SIGN itself is gone in this state, so // until SCAN SIGN — but SCAN SIGN itself is gone in this state, so
// the user must DEL → reload to ever re-enter picker phase. // the user must DEL → reload to ever re-enter picker phase.
//
// If the saved sig is reversed, also call _toggleOrientation() once
// so the stage card visually rotates 180° + the stat block swaps to
// its reversal face. The server-side `data-polarity` attribute on
// .my-sign-page already reflects the reversed flag (drives polarity-
// themed colors via the [data-polarity=...] CSS rules) but the
// visual rotation lives in the `stage-card--reversed` class which
// is JS-applied. Without this call the stage card lied: saved-
// reversed sigs rendered upright on landing while the My Sign
// applet (template-driven, reads significator_reversed directly)
// correctly rotated them — surfaces disagreed.
var savedId = pageEl.dataset.currentCardId; var savedId = pageEl.dataset.currentCardId;
if (savedId && grid) { if (savedId && grid) {
var savedCardEl = grid.querySelector( var savedCardEl = grid.querySelector(
@@ -391,6 +402,9 @@
if (savedCardEl) { if (savedCardEl) {
_populateStage(savedCardEl); _populateStage(savedCardEl);
stage.classList.add('sig-stage--frozen'); stage.classList.add('sig-stage--frozen');
if (revInput.value === '1') {
_toggleOrientation();
}
} }
} }