sea_stage reversed-card open: slow auto-rotate-in + FLIP-btn corner-swap — TDD
User spec 2026-05-26 for the sea_stage modal (shared by my_sea.html today, room.html SEA SELECT later): 1. **Reversed card opens rightside-up, then slowly auto-rotates 180°.** Preserves the original text-card legibility convention (modal opens w. upright frame + dimmed upright title + highlighted upside-down reversal title) but adds a JS-driven 0.8s rotate-in (2× the SPIN transition's 0.4s) that lands the card upside-down. Stat-block still gets `.is-reversed` immediately so the REVERSAL label is highlighted from the first frame. `_populate` no longer slaps `.stage-card--reversed` on the card up front; `_showStage` schedules `_autoRotateToReversed` 400ms post-flip-in (past the 0.35s `sea-flip-in` keyframe + buffer). Auto-rotate mirrors the SPIN-click pattern — strip the flip-in keyframe class, force reflow, inline-override `transition-duration` to 0.8s, then toggle `.stage-card--reversed` so the static `transition: transform` lerps the rotation. Timers tracked on module-level handles so dismiss-mid-rotate + reopen-mid-rotate cancel cleanly w.o. stacking handlers (cleared in `_populate`, `_hideStage`, `_testInit`). 2. **FLIP btn always lands at visual bottom-left, regardless of reversal.** Previously the in-card btn rode along w. the 180° rotation, ending top-right (user-flagged as wrong: "the game_kit.html carousel already handles this perfectly—FLIP only ever appears bottom-left, regardless of reversal"). The fan-flip-btn pulls this off by being a SIBLING of `.tarot-fan-wrap` (lives outside any rotating card) — sea_stage's btn sits INSIDE `.sea-stage-card` along w. my_sign / my_sign-applet's shared DOM, so restructuring out wasn't an option. Solved via CSS counter-positioning instead: `.sea-stage-card.stage-card--reversed .sea-stage-flip-btn` re-anchors to card-local top-right + counter-rotates 180° on the btn itself, landing it at visual bottom-left w. upright label. Companion `[data-spinning]` attr (joined to the existing flip-btn-mid-flip selector chain) hides the btn during the rotation window so it never jumps visibly between corners. Set by both `_autoRotateToReversed` (0.8s window) + the SPIN click handler (0.4s window). TDD coverage — `SeaDealSpec.js` gets a new describe block w. jasmine.clock-driven specs: - `reversed-card open` × 6: `is-reversed` set immediately on stat-block; `.stage-card--reversed` NOT set immediately on card; `data-spinning` NOT set pre-flip-in; both set after 500ms; both cleared (well, `.stage-card--reversed` persists) after 1400ms; upright cards don't trigger auto-rotate at all - `SPIN click hides FLIP via [data-spinning]` × 2: set on click; cleared after 500ms Files: - `apps/epic/static/apps/epic/sea.js` — `_populate` defers `.stage-card--reversed` + clears in-flight rotate state; `_showStage` schedules `_autoRotateToReversed` for reversed cards; SPIN handler sets `data-spinning` for the SPIN_MS window; `_hideStage` + `_testInit` clear rotate timers + spin attr; new module-level timer handles + duration constants - `static_src/scss/_card-deck.scss` — `.sea-stage-card.stage-card--reversed .sea-stage-flip-btn` counter-positioning rule (bottom→top, left→right, transform: rotate(180deg)); `[data-spinning]` joined to the unified flip-btn mid-rotate-hide selector chain - `static_src/tests/SeaDealSpec.js` + `static/tests/SeaDealSpec.js` — new describe blocks for the two new behaviors Jasmine FT green. User-verified visually on `/gameboard/my-sea/` w. an upside-down reversed-card open: "Visually verified just now in my_sea.html, very nicely done". Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -296,4 +296,85 @@ describe("SeaDeal", () => {
|
||||
expect(rank.textContent).toBe("Q");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Reversed-card auto-rotate on modal open ─────────────────────────────── //
|
||||
//
|
||||
// Spec 2026-05-26: a reversed card opens rightside-up (so the user sees
|
||||
// the upright card frame + a dimmed upright title + a highlighted
|
||||
// upside-down reversal title) and then SLOWLY auto-rotates 180° to land
|
||||
// upside-down. Duration is 2× the normal SPIN transition (0.4s → 0.8s).
|
||||
// The stat-block gets `.is-reversed` immediately (REVERSAL label
|
||||
// highlights as the modal opens); the card itself gets `.stage-card--
|
||||
// reversed` only after the auto-rotate lands. `[data-spinning]` is set
|
||||
// for the rotation window so the FLIP btn hides during the spin (the
|
||||
// SCSS-side companion rule also repositions FLIP to visual bottom-left
|
||||
// post-rotation, matching the game-kit fan precedent).
|
||||
|
||||
describe("reversed-card open", () => {
|
||||
const REVERSED_CARD = Object.assign({}, CARD, { reversed: true });
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
makeFixture();
|
||||
});
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it("sets is-reversed on stat-block immediately", () => {
|
||||
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
|
||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT set stage-card--reversed immediately (card starts rightside-up)", () => {
|
||||
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT set data-spinning before the flip-in completes", () => {
|
||||
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
|
||||
expect(stageCard.dataset.spinning).toBeUndefined();
|
||||
});
|
||||
|
||||
it("kicks off the auto-rotate after the flip-in window — data-spinning set + stage-card--reversed added", () => {
|
||||
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
|
||||
jasmine.clock().tick(500); // past sea-flip-in (0.35s) + buffer
|
||||
expect(stageCard.dataset.spinning).toBe("1");
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears data-spinning once the auto-rotate window has elapsed", () => {
|
||||
SeaDeal.openStage(REVERSED_CARD, ".sea-pos-cover", true);
|
||||
jasmine.clock().tick(500 + 900); // past flip-in + 0.8s rotate
|
||||
expect(stageCard.dataset.spinning).toBeUndefined();
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT auto-rotate an upright card", () => {
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
jasmine.clock().tick(2000);
|
||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||
expect(stageCard.dataset.spinning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPIN click hides FLIP during rotation ───────────────────────────────── //
|
||||
|
||||
describe("SPIN click hides FLIP via [data-spinning]", () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
makeFixture();
|
||||
SeaDeal.openStage(CARD, ".sea-pos-cover", true);
|
||||
});
|
||||
afterEach(() => jasmine.clock().uninstall());
|
||||
|
||||
it("sets data-spinning on SPIN click so the FLIP btn hides during the rotation", () => {
|
||||
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(stageCard.dataset.spinning).toBe("1");
|
||||
});
|
||||
|
||||
it("clears data-spinning after the SPIN transition window elapses", () => {
|
||||
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
jasmine.clock().tick(500); // past 0.4s SPIN transition
|
||||
expect(stageCard.dataset.spinning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user