My Sea form col + SPREAD dropdown w. 3-card/6-card section dividers — Sprint 5 iter 3 of My Sea roadmap — TDD

Picker phase form col: SPREAD combobox w. 6 spread options under 2 horizontal section dividers ("3-card spreads" / "6-card spreads"), reversal-% caption, GRAVITY + LEVITY deck swatches, LOCK HAND + DEL btns. Default = Situation, Action, Outcome (a 3-card spread). Selecting a 6-card spread (Celtic Cross Waite-Smith or Escape Velocity) swaps `.my-sea-cross[data-spread-shape]` from `three-card` to `six-card` — revealing the crown / lay / cross cells that the default 3-card variants hide.

Naming correction (user-locked): the spread itself is a "three-card spread" not a "three-card cross" — "cross" stays scoped to the Celtic Cross variants (6-card spreads). CSS class `.my-sea-cross` carries grid-container semantics regardless of which spread shape is active; the spread-vs-cross distinction lives at the spread-name layer only.

- **View** (gameboard/views.py): `my_sea` adds `default_spread = "situation-action-outcome"` + `reversals_pct = 25` context keys.
- **Template** (my_sea.html): renders all 7 cross cells (crown/leave/core+cover+cross/loom/lay) unconditionally + adds `data-spread-shape="three-card"` to `.my-sea-cross`. Form col DRY-reuses gameroom `_sea_overlay.html`'s `.sea-form-col` shape — `.sea-form-main` w. `.sea-field` (SPREAD label + reversal hint + custom combobox) + `.sea-stacks` (GRAVITY + LEVITY swatches) + `.sea-form-actions` (LOCK HAND + DEL). 6 options + 2 dividers in the combobox `<ul>`; dividers are `role="presentation"` so `combobox.js` skips them naturally. Inline IIFE listens for the hidden `<input id="id_sea_spread">`'s `change` event + sets `.my-sea-cross`'s `data-spread-shape` based on whether the value is in `['waite-smith', 'escape-velocity']`. No new combobox.js wiring — the existing module's `change`-bubbling contract feeds straight in.
- **SCSS** (_gameboard.scss):
  - `.my-sea-cross[data-spread-shape="three-card"]` — single-row `"leave core loom"` grid + `display: none` on crown/lay/cross.
  - `.my-sea-cross[data-spread-shape="six-card"]` — inherits the gameroom `.sea-cross`'s 3×3 grid + reveals all cells.
  - `.sea-select-divider` — section header style mirrors `.kit-bag-label`'s small-uppercase-underlined-letter-spaced --quaUser/0.75 treatment but HORIZONTAL (kit-bag uses `writing-mode: vertical-rl`; dropdown menus are flat). `pointer-events: none` belt-and-braces against accidental click/hover.
  - `.my-sea-form-col` — width-constrains the form col so the picker's cross + form sit side-by-side.

**Iter-2 contract updated** (cells in DOM, hidden via CSS for 3-card default):

- FT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_hides_six_card_only_positions_by_default` — asserts the 3 cells are in the DOM but `is_displayed() == False` so iter-3's spread switch can reveal them via CSS without re-rendering.
- IT `test_picker_does_not_render_forsaken_positions` → renamed to `test_picker_renders_six_card_only_positions_for_spread_switch` — assertContains the classes (server now renders them unconditionally).

**Tests**:

- 4 FTs in new `MySeaSpreadFormTest`: combobox renders 6 options + 2 dividers w. correct labels, default is Situation/Action/Outcome (hidden input value + visible current-label span + cross's data-spread-shape), picking Celtic Cross flips data-spread-shape to six-card + reveals crown/lay/cross, form col carries DECKS swatches + LOCK HAND + DEL + reversal-% caption. Combobox `<li>` options are inside `aria-expanded='false'` listbox → use `get_attribute("textContent")` not `.text` (which returns "" for Selenium-hidden elements).
- 7 ITs in new `MySeaSpreadFormTemplateTest`: default_spread + reversals_pct context keys, all 6 options + both labels render, 2 dividers render w. expected text, default option carries aria-selected="true", cross's initial data-spread-shape="three-card", form col DECKS + buttons + reversal hint render.

Tests: 32/32 FT green across test_bill_my_sign + test_game_my_sea; 1048/1048 IT/UT green in 52s.

Card-draw mechanics (clicking a deck swatch deposits a card into the next empty slot; LOCK HAND commits the draw) defer to iter 4 — this iter ships the spread-selection + layout-shape switch UI; the buttons are stubs (LOCK HAND starts disabled, DEL is a placeholder).

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:
Disco DeDisco
2026-05-19 17:23:25 -04:00
parent f5fc1e15f8
commit fd5db951a7
5 changed files with 417 additions and 29 deletions

View File

@@ -66,16 +66,19 @@
</div>
</div>
{# Picker phase — three-card cross w. sig pinned in core + #}
{# cover (overlaid on sig) + leave (left) + loom (right). #}
{# Crown / lay / cross from the gameroom's 6-position spread #}
{# are forsaken in the solo flow per user-locked spec. Hidden #}
{# until FREE DRAW click swaps data-phase to `picker` (see #}
{# inline JS below); form col (spread dropdown / decks / #}
{# LOCK HAND / DEL) lands in iter 3. #}
{# Picker phase — flexible spread layout. Sig pins .sea-pos- #}
{# core; the 6 surrounding positions (crown/leave/loom/lay/ #}
{# cover/cross) all render unconditionally so the SPREAD #}
{# dropdown can swap `.my-sea-cross[data-spread-shape]` #}
{# between `three-card` (default — hides crown/lay/cross via #}
{# SCSS) + `six-card` (Celtic Cross variants — shows all). #}
{# Hidden until FREE DRAW click swaps data-phase to `picker`. #}
<div class="my-sea-picker" style="display:none">
<div class="sea-cards-col">
<div class="sea-cross my-sea-cross">
<div class="sea-cross my-sea-cross" data-spread-shape="three-card">
<div class="sea-crucifix-cell sea-pos-crown">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
<div class="sea-crucifix-cell sea-pos-leave">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
@@ -88,13 +91,101 @@
<div class="sea-pos-cover">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
<div class="sea-pos-cross">
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
</div>
</div>
<div class="sea-crucifix-cell sea-pos-loom">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
<div class="sea-crucifix-cell sea-pos-lay">
<div class="sea-card-slot sea-card-slot--empty"></div>
</div>
</div>
</div>
{# Form col — SPREAD combobox + DECKS swatches + LOCK #}
{# HAND / DEL. DRY w. gameroom `_sea_overlay.html`'s #}
{# `.sea-form-col` shape; my-sea-specific differences: #}
{# (a) 6 spread options under 2 section dividers, #}
{# (b) default = situation-action-outcome (3-card), #}
{# (c) no `.sea-modal-header` (the gateway IS the page). #}
<div class="sea-form-col my-sea-form-col">
<div class="sea-form-main">
<div class="sea-field">
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
<p class="sea-reversal-hint">{{ reversals_pct|default:25 }}% reversals</p>
<input type="hidden" id="id_sea_spread" name="spread"
value="{{ default_spread }}">
<div class="sea-select"
data-combobox
data-combobox-target="id_sea_spread"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="id_sea_spread_label"
tabindex="0">
<span class="sea-select-current">Situation, Action, Outcome</span>
<span class="sea-select-arrow" aria-hidden="true"></span>
<ul class="sea-select-list" role="listbox">
<li role="presentation" class="sea-select-divider">3-card spreads</li>
<li role="option" data-value="past-present-future" aria-selected="false">Past, Present, Future</li>
<li role="option" data-value="situation-action-outcome" aria-selected="true">Situation, Action, Outcome</li>
<li role="option" data-value="mind-body-spirit" aria-selected="false">Mind, Body, Spirit</li>
<li role="option" data-value="desire-obstacle-solution" aria-selected="false">Desire, Obstacle, Solution</li>
<li role="presentation" class="sea-select-divider">6-card spreads</li>
<li role="option" data-value="waite-smith" aria-selected="false">Celtic Cross, Waite-Smith</li>
<li role="option" data-value="escape-velocity" aria-selected="false">Celtic Cross, Escape Velocity</li>
</ul>
</div>
</div>
<div class="sea-stacks">
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
</div>
<span class="sea-stack-name">Gravity</span>
</div>
<div class="sea-deck-stack sea-deck-stack--levity">
<div class="sea-stack-face">
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
</div>
</div>
<div class="sea-form-actions">
<button type="button" id="id_sea_lock_hand" class="btn btn-primary" disabled>
LOCK HAND
</button>
<button type="button" id="id_sea_del" class="btn btn-danger">
DEL
</button>
</div>
</div>
</div>
<script src="{% static 'apps/epic/combobox.js' %}"></script>
<script>
(function () {
// Spread → cross-shape mapping. Celtic Cross variants
// (Waite-Smith / Escape Velocity) get the 6-card shape;
// every other spread is 3-card (cover/leave/loom only).
var SIX_CARD_SPREADS = ['waite-smith', 'escape-velocity'];
var hidden = document.getElementById('id_sea_spread');
var cross = document.querySelector('.my-sea-cross');
if (!hidden || !cross) return;
function sync() {
var shape = SIX_CARD_SPREADS.indexOf(hidden.value) >= 0
? 'six-card' : 'three-card';
cross.setAttribute('data-spread-shape', shape);
}
hidden.addEventListener('change', sync);
sync();
}());
</script>
<script src="{% static 'apps/epic/room.js' %}"></script>
<script>