From f6093136f1c74f5d963a5b11b7245b79c4735ed3 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Fri, 22 May 2026 12:42:03 -0400 Subject: [PATCH] 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. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (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 `

` 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-`` h4 finally exercised it. Fix: drop `.tt-title` from the palette override list (left `.tt-description`/`.tt-date`/`.tt-lock` alone — those are `

` 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 `

` 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) --- src/functional_tests/test_bill_my_sign.py | 37 ++++++++--- src/static_src/scss/_billboard.scss | 80 +++++++++++++++++++++++ src/static_src/scss/_card-deck.scss | 2 - src/static_src/scss/_gameboard.scss | 3 +- src/static_src/scss/_palette-picker.scss | 11 +++- src/static_src/scss/_tooltips.scss | 8 ++- src/static_src/scss/_wallet-tokens.scss | 1 - src/templates/apps/billboard/my_sign.html | 18 +++-- 8 files changed, 138 insertions(+), 22 deletions(-) diff --git a/src/functional_tests/test_bill_my_sign.py b/src/functional_tests/test_bill_my_sign.py index f9fa09c..e8fb303 100644 --- a/src/functional_tests/test_bill_my_sign.py +++ b/src/functional_tests/test_bill_my_sign.py @@ -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 ─────────────────────────────────────────────────────────────── diff --git a/src/static_src/scss/_billboard.scss b/src/static_src/scss/_billboard.scss index 622f11e..1db9624 100644 --- a/src/static_src/scss/_billboard.scss +++ b/src/static_src/scss/_billboard.scss @@ -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; + } +} diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 568456b..4d597a4 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -675,8 +675,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; } diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index fe5e5e8..b5fcdda 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -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; } diff --git a/src/static_src/scss/_palette-picker.scss b/src/static_src/scss/_palette-picker.scss index 69814b1..c73d3a6 100644 --- a/src/static_src/scss/_palette-picker.scss +++ b/src/static_src/scss/_palette-picker.scss @@ -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 + // `

name$X

` + // 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 { diff --git a/src/static_src/scss/_tooltips.scss b/src/static_src/scss/_tooltips.scss index d7ef600..1406c5c 100644 --- a/src/static_src/scss/_tooltips.scss +++ b/src/static_src/scss/_tooltips.scss @@ -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, diff --git a/src/static_src/scss/_wallet-tokens.scss b/src/static_src/scss/_wallet-tokens.scss index 2a567f9..4fb4b84 100644 --- a/src/static_src/scss/_wallet-tokens.scss +++ b/src/static_src/scss/_wallet-tokens.scss @@ -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" diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index 9c80a63..f699f16 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -84,6 +84,11 @@ {# > .room-table-scene > .table-hex-border > .table-hex > #} {# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
+ {# 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 %}
@@ -102,6 +107,7 @@
+ {% 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,20 @@ }); } - // 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. 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'); } }