Compare commits

...

2 Commits

Author SHA1 Message Date
Disco DeDisco
1452de1a76 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>
2026-05-22 13:21:55 -04:00
Disco DeDisco
f6093136f1 fix: shop tooltip price flex-pinned right (cross-file #id_tooltip_portal .tt-title { display: block } was clobbering the flex h4) + My Sign page collapses to read-only card+stat-block when sig is saved + My Sign applet card gets proper 5:8 shell + Game Kit row space-evenly. Five visual polish items batched.
(1) **Shop tooltip price right-align — root-cause fix.** Earlier today's `feat: shop tooltip price moves to the title row` (commit e90f10f) left `.tt-price` visually adjacent to the name instead of pinned right despite `<h4 class="tt-title">` carrying flex+space-between in `_tooltips.scss:38-46`'s `.token-tooltip, .tt { h4 { ... } }` block. **The bug was in a totally unrelated file**: `_palette-picker.scss:88-95` opens `#id_tooltip_portal { .tt-title, .tt-description, .tt-date, .tt-lock { display: block; } }` for the palette swatch tooltip — `#id_tooltip_portal .tt-title` has specificity **1,1,0** which beats `.token-tooltip h4`'s **0,1,1**, ID wins regardless of source order. The palette tooltip's h4 only carries a text node (no flex children) so `block` vs `flex` looked identical for that surface + the rule sat there as quiet defensive scaffolding for months. The wallet Shop's two-`<span>` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `<p>` siblings, already block, redundant but harmless). Also keeps `margin-left: auto !important` on `.tt-price` (today's earlier failed-first-attempt fix) — now redundant w. the flex parent's space-between but documents intent + survives any future flex-direction tweak. Generalizable trap: an ID-scoped child rule in any consumer's SCSS file can silently override shared base rules for every consumer of that portal.

(2) **Game Kit row space-evenly.** `#id_game_kit` in `_gameboard.scss:61-72` was `justify-content: center` + `gap: 0.75rem` so 7 trinket icons clumped left-of-center. Switched to `justify-content: space-evenly` (no gap) — matches the established convention in `.token-row` (`_wallet-tokens.scss:46`) + `.shop-grid` (`_wallet-tokens.scss:92`) which both space-evenly the wallet's parallel rows. Items now spread across the applet width same as the wallet's tokens row + shop row.

(3) **My Sign page collapses to read-only when sig is committed.** Per user spec — once a sig is saved, the SCAN SIGN btn + table hex + chair are meaningless (you can't draw a new sig until you DEL the current one) so the landing renders only the saved-sig preview on the stage + the DEL btn pinned bottom-right. `my_sign.html:91-110` wraps the `.room-shell > .room-table > ... > #id_scan_sign_btn + .table-seat` chain in `{% if not current_significator %}`. `.my-sign-clear-form` stays unconditional (its own `{% if current_significator %}` block) — its `position: absolute; bottom: 0.75rem; right: 1rem` against the now-empty `.my-sign-landing` (which keeps `position: relative` from `_card-deck.scss:671`) lands the DEL in the same bottom-right corner whether the hex is present or not.

(4) **Stat block reveals next to saved-sig preview on landing.** `_populateStage(savedCardEl)` on init was filling the stage card data but `.sig-stat-block` stayed `display: none` because only `.sig-stage--frozen` (added by JS on OK-confirm in picker phase) reveals it via `_card-deck.scss:609`'s `&.sig-stage--frozen .sig-stat-block { display: block; }`. Added `stage.classList.add('sig-stage--frozen');` after the saved-sig `_populateStage` call at `my_sign.html:386`. Stat block now sits flex-row-adjacent to the stage card (stage's `flex-direction: row` + `gap: 0.75rem` from `_card-deck.scss:505-508` does the layout work) — emanation keywords + SPIN + FYI all visible alongside the saved card.

(5) **My Sign applet card — proper 5:8 card shell.** The applet's `<div class="my-sign-applet-card">` markup (corner-rank top-left + name) was rendering bg-less + collapsed to the applet's top-left corner because **no SCSS rule existed** for `.my-sign-applet-card` / `.my-sign-applet-body` / `.my-sign-applet-empty`. Added `#id_applet_my_sign` block at `_billboard.scss:434+` — scaled-down clone of `.sig-stage-card`'s shape language: `--applet-card-w: 5rem` knob drives all child sizing via the same calc-fractions used by `.sig-stage-card`'s `--sig-card-w`, 5:8 aspect-ratio, `--priUser` bg, `--secUser` border, corner-rank absolute top-left, `.fan-card-name` flex-centered, `&.stage-card--reversed { transform: rotate(180deg); }` for the reversed-sig case. `.my-sign-applet-body` flex-centers the card in the 4×6 applet aperture; `.my-sign-applet-empty` flex-centers the 'No sign chosen yet.' empty state. Layered visually consistent w. the room sig-select card + Shop tiles.

(6) **Misc visual cleanup bundled in.** `#id_scan_sign_btn` in `_card-deck.scss:677` lost its `font-size: 0.75rem` + `line-height: 1.1` overrides — the default `.btn-primary` sizing scales fine w. the 4rem circle now that the SCAN/SIGN wordmark fits cleanly w. just `white-space: normal`. `.tt-buy-btn` lost `line-height: 1.1` in `_wallet-tokens.scss:156` — Shop microbutton renders cleanly w. the default.

**TDD coverage**: `test_landing_previews_saved_sig_on_stage` in `test_bill_my_sign.py` rewritten to match the new contract (stage frozen → stat block visible, no SCAN SIGN btn, no `.table-hex` when sig is saved); other 28 my-sign FTs unaffected (they exercise the no-sig path which still renders the hex + SCAN SIGN). 3 traps caught + linked in memory: [[feedback-cross-file-id-scoped-override]] (this commit's #1), the pre-existing [[feedback-margin-auto-needs-flex-parent]] (correctly predicted today's bug — `!important` ladder was a tell to audit the parent's cascade), [[feedback-scss-import-order-specificity]] (related but different: same-specificity source order; this one was specificity-driven w. source order irrelevant).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:42:03 -04:00
8 changed files with 211 additions and 22 deletions

View File

@@ -280,21 +280,20 @@ class MySignPickerTest(FunctionalTest):
# ── Test 6 ───────────────────────────────────────────────────────────────
def test_landing_previews_saved_sig_on_stage(self):
"""If user already has a saved significator, the landing-phase stage
frame should preview the saved card (above/behind the hex)."""
"""If user already has a saved significator, the landing collapses to
a read-only stage: the saved card preview + its stat block (no SCAN
SIGN btn, no hex — user must DEL the saved sig to re-enter picker)."""
# Pre-save a sig in the DB
self.gamer.significator = self.target_card
self.gamer.save(update_fields=["significator"])
self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/")
# Landing phase still shows hex w. SCAN SIGN
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_scan_sign_btn")
)
# Stage card is visible w. the saved card populated (corner-rank +
# name pinned by data-card-id on the stage card wrapper)
stage_card = self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
stage_card = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
)
)
self.assertTrue(
stage_card.is_displayed(),
@@ -304,6 +303,28 @@ class MySignPickerTest(FunctionalTest):
stage_card.get_attribute("data-card-id"),
str(self.target_card.id),
)
# Stage is frozen → stat block visible next to the card
stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage")
self.assertIn("sig-stage--frozen", stage.get_attribute("class"))
self.assertTrue(
self.browser.find_element(
By.CSS_SELECTOR, ".my-sign-stage .sig-stat-block"
).is_displayed(),
"Stat block should be visible alongside the saved sig",
)
# Hex + SCAN SIGN gone — user must DEL to re-enter picker
self.assertEqual(
len(self.browser.find_elements(By.ID, "id_scan_sign_btn")),
0,
"SCAN SIGN btn should not render when a sig is already saved",
)
self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR, ".my-sign-page .table-hex"
)),
0,
"Table hex should not render when a sig is already saved",
)
# ── Test 3 ───────────────────────────────────────────────────────────────

View File

@@ -431,3 +431,83 @@ body.page-billposts {
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
// `.applet-list-page .applet-scroll`). Old `.scroll-list` styling removed.
// ── My Sign applet (billboard) ────────────────────────────────────────────
// Saved-sig preview thumbnail. Same 5:8 card shell + corner/name layout as
// `.sig-stage-card` (see `_card-deck.scss`) but scaled down to fit the 4×6
// applet aperture. Without these rules the markup collapses bg-less to the
// applet's top-left corner. `--applet-card-w` drives all child font sizing
// off a single knob (same calc-fractions as `.sig-stage-card` w. --sig-card-w).
#id_applet_my_sign {
display: flex;
flex-direction: column;
.my-sign-applet-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
.my-sign-applet-card {
--applet-card-w: 5rem;
width: var(--applet-card-w);
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 1);
border: 0.12rem solid rgba(var(--secUser), 0.6);
color: rgba(var(--secUser), 1);
padding: 0.25rem;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
transition: transform 0.4s ease;
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.05rem;
position: absolute;
top: 0.2rem;
left: 0.2rem;
.fan-corner-rank {
font-size: calc(var(--applet-card-w) * 0.18);
font-weight: 700;
}
i { font-size: calc(var(--applet-card-w) * 0.14); }
}
.fan-card-name {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
text-align: center;
font-size: calc(var(--applet-card-w) * 0.13);
font-weight: 600;
text-wrap: balance;
padding: 0 0.15rem;
color: rgba(var(--quiUser), 1);
}
&.stage-card--reversed { transform: rotate(180deg); }
}
.my-sign-applet-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
opacity: 0.6;
font-style: italic;
margin: 0;
}
}

View File

@@ -636,6 +636,65 @@ html:has(.sig-backdrop) {
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.
// 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
@@ -675,8 +734,6 @@ html:has(.sig-backdrop) {
// bumps it down a notch so the 2-line "SCAN/SIGN" label sits cleanly
// inside the 4rem circle without crowding the border.
#id_scan_sign_btn {
font-size: 0.75rem;
line-height: 1.1;
white-space: normal;
}

View File

@@ -65,8 +65,7 @@ body.page-gameboard {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.75rem;
justify-content: space-evenly;
overflow-x: visible;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }

View File

@@ -86,8 +86,15 @@
// ── Palette tooltip portal ────────────────────────────────────────────────────
#id_tooltip_portal {
// Override .tt { display: none } — portal content is shown/hidden by JS
.tt-title,
// Override .tt { display: none } — portal content is shown/hidden by JS.
// `.tt-title` deliberately NOT listed here: the shared `.token-tooltip h4
// { display: flex }` rule in _tooltips.scss must win so wallet-shop's
// `<h4 class="tt-title"><span>name</span><span class="tt-price">$X</span></h4>`
// can `justify-content: space-between` + `margin-left: auto` the price to
// the right edge. ID-scoped `display: block` here used to silently clobber
// that (1,1,0 beats 0,1,1) — bug caught 2026-05-22. The palette tooltip's
// h4 has only a text child, so leaving it flex is visually identical for
// palette while unblocking the wallet-shop layout.
.tt-description,
.tt-date,
.tt-lock {

View File

@@ -13,9 +13,11 @@
.tt-date { font-size: 1rem; color: rgba(var(--priGn), 1); }
// `.tt-price` — wallet Shop tooltip. Same shape as .tt-expiry (size +
// semantics) but --priGn for the "in the green" payment cue. Lives
// inside the `.tt-title` h4 (which is `display: flex; justify-content:
// space-between`) so the price floats top-right opposite the name.
.tt-price { font-size: 1rem; color: rgba(var(--priGn), 1); }
// inside the `.tt-title` h4 (which is `display: flex`) — `margin-left:
// auto` pushes the price to the far-right edge of the title row
// regardless of whether justify-content cascade reaches this far
// (belt + suspenders for the space-between we want).
.tt-price { font-size: 1rem; color: rgba(var(--priGn), 1); margin-left: auto !important; }
}
.token-tooltip,

View File

@@ -156,7 +156,6 @@
padding: 0.25rem 0.75rem;
white-space: normal;
word-break: normal;
line-height: 1.1;
}
// `.tt-already-owned` text — match Game Kit's "Equipped" / "In-Use: X"

View File

@@ -84,6 +84,11 @@
{# > .room-table-scene > .table-hex-border > .table-hex > #}
{# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
<div class="my-sign-landing">
{# Hex + chair only when the user has no saved sig — once a sig is #}
{# locked in, the stage-card preview + stat block above are the page #}
{# content; the user must DEL their saved sig before drawing a new #}
{# one. SCAN SIGN has no meaning while a sig is committed. #}
{% if not current_significator %}
<div class="room-shell">
<div id="id_game_table" class="room-table">
<div class="room-table-scene">
@@ -102,6 +107,7 @@
</div>
</div>
</div>
{% endif %}
{# CLEAR SIGN — only when a sig is already saved. POST to clear_sign #}
{# wipes User.significator + significator_reversed + reloads back to #}
{# the no-sig landing. Sprint 4b-adjacent. #}
@@ -371,16 +377,34 @@
});
}
// On-load: if user has a saved sig, populate the stage preview so
// the saved card is visible above the landing hex. The picker grid
// stays hidden until SCAN SIGN is clicked. The saved card is NOT
// auto-locked — that happens on entering picker phase if desired.
// On-load: if user has a saved sig, populate the stage preview AND
// reveal the stat block (via .sig-stage--frozen) so the saved card
// appears alongside its emanation/reversal keywords — the page is
// read-only on landing while a sig is committed (hex is server-side
// hidden, DEL is the only action). The picker grid stays hidden
// until SCAN SIGN — but SCAN SIGN itself is gone in this state, so
// 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;
if (savedId && grid) {
var savedCardEl = grid.querySelector(
'.sig-card[data-card-id="' + savedId + '"]');
if (savedCardEl) {
_populateStage(savedCardEl);
stage.classList.add('sig-stage--frozen');
if (revInput.value === '1') {
_toggleOrientation();
}
}
}