Compare commits
2 Commits
e90f10fe47
...
1452de1a76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1452de1a76 | ||
|
|
f6093136f1 |
@@ -280,21 +280,20 @@ class MySignPickerTest(FunctionalTest):
|
|||||||
# ── Test 6 ───────────────────────────────────────────────────────────────
|
# ── Test 6 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_landing_previews_saved_sig_on_stage(self):
|
def test_landing_previews_saved_sig_on_stage(self):
|
||||||
"""If user already has a saved significator, the landing-phase stage
|
"""If user already has a saved significator, the landing collapses to
|
||||||
frame should preview the saved card (above/behind the hex)."""
|
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
|
# Pre-save a sig in the DB
|
||||||
self.gamer.significator = self.target_card
|
self.gamer.significator = self.target_card
|
||||||
self.gamer.save(update_fields=["significator"])
|
self.gamer.save(update_fields=["significator"])
|
||||||
self.create_pre_authenticated_session(self.email)
|
self.create_pre_authenticated_session(self.email)
|
||||||
self.browser.get(self.live_server_url + "/billboard/my-sign/")
|
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 +
|
# Stage card is visible w. the saved card populated (corner-rank +
|
||||||
# name pinned by data-card-id on the stage card wrapper)
|
# name pinned by data-card-id on the stage card wrapper)
|
||||||
stage_card = self.browser.find_element(
|
stage_card = self.wait_for(
|
||||||
By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
stage_card.is_displayed(),
|
stage_card.is_displayed(),
|
||||||
@@ -304,6 +303,28 @@ class MySignPickerTest(FunctionalTest):
|
|||||||
stage_card.get_attribute("data-card-id"),
|
stage_card.get_attribute("data-card-id"),
|
||||||
str(self.target_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 ───────────────────────────────────────────────────────────────
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -431,3 +431,83 @@ body.page-billposts {
|
|||||||
|
|
||||||
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
|
// My Scrolls now rides the shared `.applet-list` rule above (lifted out of
|
||||||
// `.applet-list-page .applet-scroll`). Old `.scroll-list` styling removed.
|
// `.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -675,8 +734,6 @@ html:has(.sig-backdrop) {
|
|||||||
// bumps it down a notch so the 2-line "SCAN/SIGN" label sits cleanly
|
// bumps it down a notch so the 2-line "SCAN/SIGN" label sits cleanly
|
||||||
// inside the 4rem circle without crowding the border.
|
// inside the 4rem circle without crowding the border.
|
||||||
#id_scan_sign_btn {
|
#id_scan_sign_btn {
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.1;
|
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ body.page-gameboard {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-evenly;
|
||||||
gap: 0.75rem;
|
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
&::-webkit-scrollbar { display: none; }
|
&::-webkit-scrollbar { display: none; }
|
||||||
|
|||||||
@@ -86,8 +86,15 @@
|
|||||||
// ── Palette tooltip portal ────────────────────────────────────────────────────
|
// ── Palette tooltip portal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
#id_tooltip_portal {
|
#id_tooltip_portal {
|
||||||
// Override .tt { display: none } — portal content is shown/hidden by JS
|
// Override .tt { display: none } — portal content is shown/hidden by JS.
|
||||||
.tt-title,
|
// `.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-description,
|
||||||
.tt-date,
|
.tt-date,
|
||||||
.tt-lock {
|
.tt-lock {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
.tt-date { font-size: 1rem; color: rgba(var(--priGn), 1); }
|
.tt-date { font-size: 1rem; color: rgba(var(--priGn), 1); }
|
||||||
// `.tt-price` — wallet Shop tooltip. Same shape as .tt-expiry (size +
|
// `.tt-price` — wallet Shop tooltip. Same shape as .tt-expiry (size +
|
||||||
// semantics) but --priGn for the "in the green" payment cue. Lives
|
// semantics) but --priGn for the "in the green" payment cue. Lives
|
||||||
// inside the `.tt-title` h4 (which is `display: flex; justify-content:
|
// inside the `.tt-title` h4 (which is `display: flex`) — `margin-left:
|
||||||
// space-between`) so the price floats top-right opposite the name.
|
// auto` pushes the price to the far-right edge of the title row
|
||||||
.tt-price { font-size: 1rem; color: rgba(var(--priGn), 1); }
|
// 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,
|
.token-tooltip,
|
||||||
|
|||||||
@@ -156,7 +156,6 @@
|
|||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
line-height: 1.1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `.tt-already-owned` text — match Game Kit's "Equipped" / "In-Use: X"
|
// `.tt-already-owned` text — match Game Kit's "Equipped" / "In-Use: X"
|
||||||
|
|||||||
@@ -84,6 +84,11 @@
|
|||||||
{# > .room-table-scene > .table-hex-border > .table-hex > #}
|
{# > .room-table-scene > .table-hex-border > .table-hex > #}
|
||||||
{# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
|
{# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
|
||||||
<div class="my-sign-landing">
|
<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 class="room-shell">
|
||||||
<div id="id_game_table" class="room-table">
|
<div id="id_game_table" class="room-table">
|
||||||
<div class="room-table-scene">
|
<div class="room-table-scene">
|
||||||
@@ -102,6 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{# CLEAR SIGN — only when a sig is already saved. POST to clear_sign #}
|
{# CLEAR SIGN — only when a sig is already saved. POST to clear_sign #}
|
||||||
{# wipes User.significator + significator_reversed + reloads back to #}
|
{# wipes User.significator + significator_reversed + reloads back to #}
|
||||||
{# the no-sig landing. Sprint 4b-adjacent. #}
|
{# the no-sig landing. Sprint 4b-adjacent. #}
|
||||||
@@ -371,16 +377,34 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// On-load: if user has a saved sig, populate the stage preview so
|
// On-load: if user has a saved sig, populate the stage preview AND
|
||||||
// the saved card is visible above the landing hex. The picker grid
|
// reveal the stat block (via .sig-stage--frozen) so the saved card
|
||||||
// stays hidden until SCAN SIGN is clicked. The saved card is NOT
|
// appears alongside its emanation/reversal keywords — the page is
|
||||||
// auto-locked — that happens on entering picker phase if desired.
|
// 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;
|
var savedId = pageEl.dataset.currentCardId;
|
||||||
if (savedId && grid) {
|
if (savedId && grid) {
|
||||||
var savedCardEl = grid.querySelector(
|
var savedCardEl = grid.querySelector(
|
||||||
'.sig-card[data-card-id="' + savedId + '"]');
|
'.sig-card[data-card-id="' + savedId + '"]');
|
||||||
if (savedCardEl) {
|
if (savedCardEl) {
|
||||||
_populateStage(savedCardEl);
|
_populateStage(savedCardEl);
|
||||||
|
stage.classList.add('sig-stage--frozen');
|
||||||
|
if (revInput.value === '1') {
|
||||||
|
_toggleOrientation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user