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:
@@ -618,13 +618,17 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
|||||||
self.assertContains(response, "sea-pos-leave")
|
self.assertContains(response, "sea-pos-leave")
|
||||||
self.assertContains(response, "sea-pos-loom")
|
self.assertContains(response, "sea-pos-loom")
|
||||||
|
|
||||||
def test_picker_does_not_render_forsaken_positions(self):
|
def test_picker_renders_six_card_only_positions_for_spread_switch(self):
|
||||||
# Crown / lay / cross are gameroom-only — user-locked spec drops
|
# Crown / lay / cross sit in the DOM unconditionally so iter 3's
|
||||||
# them from the solo three-card spread.
|
# SPREAD dropdown can reveal them via CSS attribute swap (data-
|
||||||
|
# spread-shape="six-card") without re-rendering. Default 3-card
|
||||||
|
# spread hides them via `.my-sea-cross[data-spread-shape=
|
||||||
|
# "three-card"]` rules in _gameboard.scss — FT pins the hidden
|
||||||
|
# state visually.
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertNotContains(response, "sea-pos-crown")
|
self.assertContains(response, "sea-pos-crown")
|
||||||
self.assertNotContains(response, "sea-pos-lay")
|
self.assertContains(response, "sea-pos-lay")
|
||||||
self.assertNotContains(response, "sea-pos-cross")
|
self.assertContains(response, "sea-pos-cross")
|
||||||
|
|
||||||
def test_picker_not_rendered_when_user_has_no_sig(self):
|
def test_picker_not_rendered_when_user_has_no_sig(self):
|
||||||
# 4b gate wins; picker has no business rendering without a sig.
|
# 4b gate wins; picker has no business rendering without a sig.
|
||||||
@@ -632,3 +636,75 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
|||||||
self.user.save(update_fields=["significator"])
|
self.user.save(update_fields=["significator"])
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertNotContains(response, "my-sea-picker")
|
self.assertNotContains(response, "my-sea-picker")
|
||||||
|
|
||||||
|
|
||||||
|
class MySeaSpreadFormTemplateTest(TestCase):
|
||||||
|
"""Sprint 5 iter 3 — form col + SPREAD dropdown structure + default-
|
||||||
|
spread context + cross's `data-spread-shape` attribute. Iter 3 spec
|
||||||
|
locks `Situation, Action, Outcome` as the default spread (a 3-card
|
||||||
|
variant); the 6 spreads sit under 2 section dividers (3-card / 6-
|
||||||
|
card)."""
|
||||||
|
|
||||||
|
SPREAD_OPTIONS = [
|
||||||
|
("past-present-future", "Past, Present, Future"),
|
||||||
|
("situation-action-outcome", "Situation, Action, Outcome"),
|
||||||
|
("mind-body-spirit", "Mind, Body, Spirit"),
|
||||||
|
("desire-obstacle-solution", "Desire, Obstacle, Solution"),
|
||||||
|
("waite-smith", "Celtic Cross, Waite-Smith"),
|
||||||
|
("escape-velocity", "Celtic Cross, Escape Velocity"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from apps.epic.models import personal_sig_cards
|
||||||
|
self.user = User.objects.create(email="spread@test.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.target = personal_sig_cards(self.user)[0]
|
||||||
|
self.user.significator = self.target
|
||||||
|
self.user.save(update_fields=["significator"])
|
||||||
|
|
||||||
|
def test_context_default_spread_is_situation_action_outcome(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["default_spread"], "situation-action-outcome",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_context_reversals_pct_defaults_to_25(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertEqual(response.context["reversals_pct"], 25)
|
||||||
|
|
||||||
|
def test_template_renders_all_six_spread_options(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
html = response.content.decode()
|
||||||
|
for value, label in self.SPREAD_OPTIONS:
|
||||||
|
with self.subTest(spread=value):
|
||||||
|
self.assertIn(f'data-value="{value}"', html)
|
||||||
|
self.assertIn(label, html)
|
||||||
|
|
||||||
|
def test_template_renders_three_card_and_six_card_section_dividers(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
html = response.content.decode()
|
||||||
|
self.assertEqual(html.count("sea-select-divider"), 2)
|
||||||
|
self.assertIn("3-card spreads", html)
|
||||||
|
self.assertIn("6-card spreads", html)
|
||||||
|
|
||||||
|
def test_template_marks_situation_action_outcome_aria_selected(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
html = response.content.decode()
|
||||||
|
# The default option carries aria-selected="true"; the others false.
|
||||||
|
self.assertIn(
|
||||||
|
'data-value="situation-action-outcome" aria-selected="true"', html,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cross_carries_initial_three_card_spread_shape(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertContains(response, 'data-spread-shape="three-card"')
|
||||||
|
|
||||||
|
def test_form_col_renders_decks_lock_hand_del_and_reversal_hint(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
html = response.content.decode()
|
||||||
|
self.assertIn("sea-deck-stack--gravity", html)
|
||||||
|
self.assertIn("sea-deck-stack--levity", html)
|
||||||
|
self.assertIn('id="id_sea_lock_hand"', html)
|
||||||
|
self.assertIn('id="id_sea_del"', html)
|
||||||
|
self.assertIn("sea-reversal-hint", html)
|
||||||
|
self.assertIn("25% reversals", html)
|
||||||
|
|||||||
@@ -193,6 +193,11 @@ def my_sea(request):
|
|||||||
# `.suit_icon` resolve at render time.
|
# `.suit_icon` resolve at render time.
|
||||||
"significator": request.user.significator,
|
"significator": request.user.significator,
|
||||||
"significator_reversed": request.user.significator_reversed,
|
"significator_reversed": request.user.significator_reversed,
|
||||||
|
# Sprint 5 iter 3 — SPREAD dropdown defaults to Situation/Action/
|
||||||
|
# Outcome (a 3-card spread) per user-locked spec; `reversals_pct`
|
||||||
|
# is a placeholder UI value pending the per-user setting.
|
||||||
|
"default_spread": "situation-action-outcome",
|
||||||
|
"reversals_pct": 25,
|
||||||
"page_class": "page-gameboard page-my-sea",
|
"page_class": "page-gameboard page-my-sea",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -399,15 +399,191 @@ class MySeaPickerPhaseTest(FunctionalTest):
|
|||||||
|
|
||||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_picker_does_not_render_forsaken_positions(self):
|
def test_picker_hides_six_card_only_positions_by_default(self):
|
||||||
"""Crown / lay / cross from the gameroom's 6-position Celtic
|
"""Crown / lay / cross are 6-card-spread-only positions; they
|
||||||
Cross are forsaken in the solo three-card spread (user-locked
|
sit in the picker DOM (so iter 3's spread switch can reveal
|
||||||
spec). None of these classes should appear in the picker DOM."""
|
them without re-rendering) but stay `display:none` while the
|
||||||
|
default 3-card spread (`data-spread-shape="three-card"`) is
|
||||||
|
active. Iter 3's SPREAD dropdown switches `data-spread-shape`
|
||||||
|
on change to reveal these positions for Celtic Cross variants."""
|
||||||
picker = self._enter_picker_phase()
|
picker = self._enter_picker_phase()
|
||||||
for forsaken in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
|
for hidden_pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
|
||||||
with self.subTest(position=forsaken):
|
with self.subTest(position=hidden_pos):
|
||||||
|
elements = picker.find_elements(By.CSS_SELECTOR, hidden_pos)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(picker.find_elements(By.CSS_SELECTOR, forsaken)),
|
len(elements), 1,
|
||||||
0,
|
f"{hidden_pos} should render in DOM (gets shown on 6-card spread switch)",
|
||||||
f"forsaken position {forsaken} should not render in my-sea picker",
|
|
||||||
)
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
elements[0].is_displayed(),
|
||||||
|
f"{hidden_pos} should be hidden in default 3-card spread",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MySeaSpreadFormTest(FunctionalTest):
|
||||||
|
"""Sprint 5 iter 3 — form col on the picker phase: SPREAD dropdown
|
||||||
|
(custom combobox w. 6 options + 2 horizontal section dividers for
|
||||||
|
"3-card spreads" / "6-card spreads"), reversal-rate caption, two
|
||||||
|
DECKS swatches (GRAVITY + LEVITY), LOCK HAND + DEL btns. Selecting
|
||||||
|
a 6-card spread (Celtic Cross variants) swaps `.my-sea-cross[data-
|
||||||
|
spread-shape]` from `three-card` to `six-card`, revealing the
|
||||||
|
crown / lay / cross positions hidden by default.
|
||||||
|
|
||||||
|
Card-draw mechanics — clicking a deck swatch to deposit a card into
|
||||||
|
the next empty slot — defers to iter 4."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
_seed_earthman_sig_pile()
|
||||||
|
_seed_gameboard_applets()
|
||||||
|
self.email = "spread@test.io"
|
||||||
|
self.gamer = User.objects.create(email=self.email)
|
||||||
|
self.target_card = _assign_sig(self.gamer)
|
||||||
|
|
||||||
|
def _enter_picker_phase(self):
|
||||||
|
self.create_pre_authenticated_session(self.email)
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||||
|
btn = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_draw_sea_btn")
|
||||||
|
)
|
||||||
|
btn.click()
|
||||||
|
return self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sea-page[data-phase='picker']"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Test 1 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_spread_dropdown_renders_six_options_and_two_dividers(self):
|
||||||
|
"""SPREAD combobox has 4 three-card options + 2 six-card
|
||||||
|
options + 2 horizontal section dividers labelled "3-card
|
||||||
|
spreads" / "6-card spreads". Dividers are `role=presentation`
|
||||||
|
+ `.sea-select-divider` so combobox.js skips them.
|
||||||
|
|
||||||
|
The dropdown is closed (`aria-expanded='false'`) on initial
|
||||||
|
render so the <li>s aren't displayed; use textContent rather
|
||||||
|
than `.text` (which returns "" for hidden elements)."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
options = picker.find_elements(
|
||||||
|
By.CSS_SELECTOR, ".sea-select-list [role='option']"
|
||||||
|
)
|
||||||
|
self.assertEqual(len(options), 6)
|
||||||
|
option_labels = [
|
||||||
|
o.get_attribute("textContent").strip() for o in options
|
||||||
|
]
|
||||||
|
# Three-card variants — labels per [[project-my-sea-roadmap]]
|
||||||
|
# iter 3 spec lock.
|
||||||
|
self.assertIn("Past, Present, Future", option_labels)
|
||||||
|
self.assertIn("Situation, Action, Outcome", option_labels)
|
||||||
|
self.assertIn("Mind, Body, Spirit", option_labels)
|
||||||
|
self.assertIn("Desire, Obstacle, Solution", option_labels)
|
||||||
|
# Six-card variants
|
||||||
|
self.assertIn("Celtic Cross, Waite-Smith", option_labels)
|
||||||
|
self.assertIn("Celtic Cross, Escape Velocity", option_labels)
|
||||||
|
# Two horizontal dividers
|
||||||
|
dividers = picker.find_elements(By.CSS_SELECTOR, ".sea-select-divider")
|
||||||
|
self.assertEqual(len(dividers), 2)
|
||||||
|
divider_text = "|".join(
|
||||||
|
d.get_attribute("textContent").upper().strip() for d in dividers
|
||||||
|
)
|
||||||
|
self.assertIn("3-CARD SPREADS", divider_text)
|
||||||
|
self.assertIn("6-CARD SPREADS", divider_text)
|
||||||
|
|
||||||
|
# ── Test 2 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_default_spread_is_situation_action_outcome(self):
|
||||||
|
"""Per the spec, `Situation, Action, Outcome` is the default
|
||||||
|
spread on landing — selected in the combobox + reflected in
|
||||||
|
the hidden `<input id="id_sea_spread">` initial value. The
|
||||||
|
combobox's `.sea-select-current` `<span>` carries the visible
|
||||||
|
label (rendered even when the dropdown is closed)."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread")
|
||||||
|
self.assertEqual(
|
||||||
|
hidden.get_attribute("value"), "situation-action-outcome",
|
||||||
|
)
|
||||||
|
# Selected <li> is inside the closed dropdown — its `.text` is
|
||||||
|
# "" per Selenium's hidden-text convention; use textContent.
|
||||||
|
selected = picker.find_element(
|
||||||
|
By.CSS_SELECTOR, ".sea-select-list [role='option'][aria-selected='true']"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
selected.get_attribute("textContent").strip(),
|
||||||
|
"Situation, Action, Outcome",
|
||||||
|
)
|
||||||
|
# The visible current-label span is always rendered.
|
||||||
|
current = picker.find_element(By.CSS_SELECTOR, ".sea-select-current")
|
||||||
|
self.assertEqual(current.text.strip(), "Situation, Action, Outcome")
|
||||||
|
# And the cross is in three-card shape mode by default.
|
||||||
|
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
|
||||||
|
self.assertEqual(cross.get_attribute("data-spread-shape"), "three-card")
|
||||||
|
|
||||||
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_picking_celtic_cross_reveals_six_card_positions(self):
|
||||||
|
"""Clicking a 6-card spread option swaps `data-spread-shape`
|
||||||
|
to `six-card` on `.my-sea-cross` — crown / lay / cross become
|
||||||
|
visible. Three-card variants flip back to `three-card`."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
|
||||||
|
# Open the combobox + click Celtic Cross Waite-Smith
|
||||||
|
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
|
||||||
|
combo.click()
|
||||||
|
waite_smith = picker.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
".sea-select-list [role='option'][data-value='waite-smith']",
|
||||||
|
)
|
||||||
|
waite_smith.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertEqual(
|
||||||
|
cross.get_attribute("data-spread-shape"), "six-card"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
|
||||||
|
with self.subTest(position=pos):
|
||||||
|
self.assertTrue(
|
||||||
|
picker.find_element(By.CSS_SELECTOR, pos).is_displayed(),
|
||||||
|
f"{pos} should be visible when 6-card spread selected",
|
||||||
|
)
|
||||||
|
# Switch back to a three-card spread → hidden again.
|
||||||
|
combo.click()
|
||||||
|
mbs = picker.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
".sea-select-list [role='option'][data-value='mind-body-spirit']",
|
||||||
|
)
|
||||||
|
mbs.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertEqual(
|
||||||
|
cross.get_attribute("data-spread-shape"), "three-card"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Test 4 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_form_col_renders_decks_lock_hand_del_and_reversal_pct(self):
|
||||||
|
"""Form col carries the DECKS swatches (GRAVITY + LEVITY), the
|
||||||
|
LOCK HAND `.btn-primary`, the DEL `.btn-danger`, and the
|
||||||
|
reversal-percentage caption (default 25%)."""
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
# DECKS — two stacks
|
||||||
|
stacks = picker.find_elements(By.CSS_SELECTOR, ".sea-deck-stack")
|
||||||
|
self.assertEqual(len(stacks), 2)
|
||||||
|
names = "|".join(
|
||||||
|
s.find_element(By.CSS_SELECTOR, ".sea-stack-name").text.upper()
|
||||||
|
for s in stacks
|
||||||
|
)
|
||||||
|
self.assertIn("GRAVITY", names)
|
||||||
|
self.assertIn("LEVITY", names)
|
||||||
|
# LOCK HAND + DEL
|
||||||
|
lock = picker.find_element(By.CSS_SELECTOR, "#id_sea_lock_hand")
|
||||||
|
self.assertIn("LOCK", lock.text.upper())
|
||||||
|
self.assertIn("HAND", lock.text.upper())
|
||||||
|
self.assertIn("btn-primary", lock.get_attribute("class"))
|
||||||
|
delbtn = picker.find_element(By.CSS_SELECTOR, "#id_sea_del")
|
||||||
|
self.assertIn("DEL", delbtn.text.upper())
|
||||||
|
self.assertIn("btn-danger", delbtn.get_attribute("class"))
|
||||||
|
# Reversal % caption — default 25
|
||||||
|
hint = picker.find_element(By.CSS_SELECTOR, ".sea-reversal-hint")
|
||||||
|
self.assertIn("25", hint.text)
|
||||||
|
self.assertIn("reversal", hint.text.lower())
|
||||||
|
|||||||
@@ -282,13 +282,53 @@ body.page-gameboard {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Three-cell horizontal row (leave | core | loom) — strips the crown
|
// .my-sea-cross renders all 6 surrounding positions (crown/lay/cross
|
||||||
// + lay rows from `.sea-cross`'s 3×3 grid template, leaving only the
|
// PLUS leave/loom/cover) so the SPREAD dropdown can toggle 3-card vs
|
||||||
// middle row. Cover stays nested inside `.sea-pos-core` (absolutely
|
// 6-card layout via a single data-attribute swap (no DOM mutation).
|
||||||
// positioned overlay handled by _card-deck.scss line 1310-1331).
|
// Default `three-card` hides the 3 forsaken positions via `display:
|
||||||
.my-sea-cross {
|
// none`; `six-card` (Celtic Cross variants) renders all 7 cells.
|
||||||
grid-template-areas: "leave core loom";
|
// Cover always shows (it's part of both shapes — overlaid on sig).
|
||||||
|
.my-sea-cross[data-spread-shape="three-card"] {
|
||||||
|
grid-template-areas:
|
||||||
|
"leave core loom";
|
||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
|
|
||||||
|
.sea-pos-crown,
|
||||||
|
.sea-pos-lay,
|
||||||
|
.sea-pos-cross { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-sea-cross[data-spread-shape="six-card"] {
|
||||||
|
// Inherits `.sea-cross`'s 3×3 template from _card-deck.scss line 1189-
|
||||||
|
// 1200; nothing to override layout-wise. All cells visible.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
||||||
|
// / "6-card spreads" separating the option groups. Styled to echo the
|
||||||
|
// `.kit-bag-label` treatment (small uppercase underlined letter-spaced
|
||||||
|
// --quaUser) but horizontal rather than vertical (kit-bag uses writing-
|
||||||
|
// mode: vertical-rl; this is a flat dropdown).
|
||||||
|
.sea-select-list .sea-select-divider {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: underline;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: rgba(var(--quaUser), 0.75);
|
||||||
|
padding: 0.4rem 0.6rem 0.2rem;
|
||||||
|
pointer-events: none; // not selectable; combobox.js skips it
|
||||||
|
// (no role=option), but belt-and-braces
|
||||||
|
// against accidental hover/click styles.
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form col on my-sea — same DRY treatment as the gameroom sea-overlay
|
||||||
|
// `.sea-form-col` (handled in _card-deck.scss) but sits next to the
|
||||||
|
// picker's cross on a `--duoUser` page. Just constrain the width so it
|
||||||
|
// doesn't fight the cross for horizontal space.
|
||||||
|
.my-sea-form-col {
|
||||||
|
flex: 0 0 16rem;
|
||||||
|
max-width: 16rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,16 +66,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Picker phase — three-card cross w. sig pinned in core + #}
|
{# Picker phase — flexible spread layout. Sig pins .sea-pos- #}
|
||||||
{# cover (overlaid on sig) + leave (left) + loom (right). #}
|
{# core; the 6 surrounding positions (crown/leave/loom/lay/ #}
|
||||||
{# Crown / lay / cross from the gameroom's 6-position spread #}
|
{# cover/cross) all render unconditionally so the SPREAD #}
|
||||||
{# are forsaken in the solo flow per user-locked spec. Hidden #}
|
{# dropdown can swap `.my-sea-cross[data-spread-shape]` #}
|
||||||
{# until FREE DRAW click swaps data-phase to `picker` (see #}
|
{# between `three-card` (default — hides crown/lay/cross via #}
|
||||||
{# inline JS below); form col (spread dropdown / decks / #}
|
{# SCSS) + `six-card` (Celtic Cross variants — shows all). #}
|
||||||
{# LOCK HAND / DEL) lands in iter 3. #}
|
{# Hidden until FREE DRAW click swaps data-phase to `picker`. #}
|
||||||
<div class="my-sea-picker" style="display:none">
|
<div class="my-sea-picker" style="display:none">
|
||||||
<div class="sea-cards-col">
|
<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-crucifix-cell sea-pos-leave">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,13 +91,101 @@
|
|||||||
<div class="sea-pos-cover">
|
<div class="sea-pos-cover">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sea-pos-cross">
|
||||||
|
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-loom">
|
<div class="sea-crucifix-cell sea-pos-loom">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty"></div>
|
||||||
</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>
|
</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 src="{% static 'apps/epic/room.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user