My Sea per-spread positions + draw-order JS config + position labels — Sprint 5 iter 3 follow-up — TDD
User-locked spec 2026-05-19: each three-card spread uses a DIFFERENT 3-position subset of the 6 surrounding positions, in its own draw order. Replaces the iter-3 binary `data-spread-shape="three-card|six-card"` model w. per-spread `data-spread="<value>"`. Closes iter 3 cleanly + scaffolds the draw-order data iter 4 will consume. Position subsets (per spread): PPF → leave (1) · cover (2) · loom (3) SAO → lay (1) · cover (2) · crown (3) MBS → crown (1) · lay (2) · loom (3) DOS → loom (1) · cross (2) · cover (3) Waite-Smith → all 6 surrounding (cover · cross · crown · lay · loom · leave) Escape Velocity → all 6 surrounding (cover · cross · lay · leave · crown · loom) All 6 cells continue to render in DOM unconditionally — `.my-sea-cross[data-spread="<value>"]` SCSS rules hide inactive positions per spread via `display: none`. Cover/cross live nested inside `.sea-pos-core` so their absolute-overlay positioning rules from `_card-deck.scss:1310-1331` carry over for free. **Position labels** (re-appropriated `.sea-stack-name` typography per user) — `.sea-pos-label` inside each empty `.sea-card-slot--empty` carries the per-spread caption. Server-renders SAO's labels by default (lay=Situation, cover=Action, crown=Outcome); JS swaps labels via `POSITION_LABELS[spread]` lookup on combobox change. Inactive-for-spread positions render their span w. empty `textContent` so JS only has to set text, never toggle visibility. Celtic Cross variants share the gameroom's existing position vocabulary (Crown/Beneath/Cover/Cross/Before/Behind). **DRAW_ORDER JS const** baked into the inline picker IIFE — array of position names per spread, ready for iter 4's deck-click-deposit logic to consume. Exposed via `window._mySeaDrawOrder` so iter-4 click handlers can `window._mySeaDrawOrder[currentSpread][nextSlotIdx]` to resolve the target position. No click handlers wired yet — iter 4 territory. **Selenium trap caught**: the combobox click-twice-on-the-toggle bug — re-clicking the combobox while `aria-expanded='true'` closes the dropdown (combobox.js's toggle behavior). Test 3's spread-cycling iterates through 6 spreads, each needs the dropdown OPEN before clicking a new option; added a `_pick(value)` helper that checks `aria-expanded` first. Files: - `templates/apps/gameboard/my_sea.html` — `.my-sea-cross[data-spread]` w. server-rendered default; each empty slot wraps a `<span class="sea-pos-label" data-position="<name>">` (SAO labels seeded inline, others empty initially); inline IIFE adds `DRAW_ORDER` + `POSITION_LABELS` consts + `syncLabels()` that swaps captions on `change`. - `static_src/scss/_gameboard.scss` — drops the `data-spread-shape="three-card"|"six-card"` rules; adds 4 per-spread visibility rules (PPF/SAO/MBS/DOS). Celtic Cross variants inherit the gameroom's full 3×3 grid w. no overrides. `.sea-pos-label` style mirrors `.sea-stack-name` from _card-deck.scss line 1557 (small-uppercase-letter-spaced-scaleY) sans the polarity color — these aren't deck identifiers, just spread-position captions. - `apps/gameboard/tests/integrated/test_views.py` — IT `test_cross_carries_initial_three_card_spread_shape` renamed + retargeted to `data-spread="situation-action-outcome"`; new IT `test_template_renders_sao_position_labels_on_default` pins the seeded SAO labels + empty spans for inactive positions. - `functional_tests/test_game_my_sea.py` — iter-2's `test_picker_hides_six_card_only_positions_by_default` renamed to `test_picker_renders_sao_default_position_subset` w. SAO-specific visibility expectations (lay/cover/crown visible; leave/loom/cross hidden). iter-3's `test_picking_celtic_cross_reveals_six_card_positions` rewritten + expanded to `test_picking_spread_swaps_data_spread_and_position_visibility` — cycles through all 6 spreads, asserts `data-spread` attribute + per-position `is_displayed()` for each. New `test_per_spread_position_labels_render_and_update` cycles through 5 spreads (SAO default + 4 switches) asserting captions match the spec. Tests: 33/33 FT green across test_bill_my_sign + test_game_my_sea; 1049/1049 IT/UT green in 52s. 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:
@@ -695,9 +695,27 @@ class MySeaSpreadFormTemplateTest(TestCase):
|
|||||||
'data-value="situation-action-outcome" aria-selected="true"', html,
|
'data-value="situation-action-outcome" aria-selected="true"', html,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_cross_carries_initial_three_card_spread_shape(self):
|
def test_cross_carries_initial_data_spread_sao(self):
|
||||||
|
# `.my-sea-cross[data-spread]` is the per-spread visibility key;
|
||||||
|
# default-spread context value renders into the attribute. SCSS
|
||||||
|
# rules in _gameboard.scss hide the inactive positions per spread.
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertContains(response, 'data-spread-shape="three-card"')
|
self.assertContains(response, 'data-spread="situation-action-outcome"')
|
||||||
|
|
||||||
|
def test_template_renders_sao_position_labels_on_default(self):
|
||||||
|
# Server-renders the SAO position labels into the empty drop-zone
|
||||||
|
# `.sea-pos-label` spans so the page is correct before JS boots.
|
||||||
|
# JS swaps labels on spread change.
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
html = response.content.decode()
|
||||||
|
self.assertIn('data-position="lay">Situation</span>', html)
|
||||||
|
self.assertIn('data-position="cover">Action</span>', html)
|
||||||
|
self.assertIn('data-position="crown">Outcome</span>', html)
|
||||||
|
# Inactive-for-SAO positions render their span but w. empty
|
||||||
|
# textContent (JS fills them on spread switch).
|
||||||
|
self.assertIn('data-position="leave"></span>', html)
|
||||||
|
self.assertIn('data-position="loom"></span>', html)
|
||||||
|
self.assertIn('data-position="cross"></span>', html)
|
||||||
|
|
||||||
def test_form_col_renders_decks_lock_hand_del_and_reversal_hint(self):
|
def test_form_col_renders_decks_lock_hand_del_and_reversal_hint(self):
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
|||||||
@@ -399,24 +399,24 @@ class MySeaPickerPhaseTest(FunctionalTest):
|
|||||||
|
|
||||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_picker_hides_six_card_only_positions_by_default(self):
|
def test_picker_renders_sao_default_position_subset(self):
|
||||||
"""Crown / lay / cross are 6-card-spread-only positions; they
|
"""Default spread = Situation/Action/Outcome (SAO) → only lay
|
||||||
sit in the picker DOM (so iter 3's spread switch can reveal
|
(Situation) + cover (Action) + crown (Outcome) visible from the
|
||||||
them without re-rendering) but stay `display:none` while the
|
6 surrounding positions; leave / loom / cross hidden. All 6
|
||||||
default 3-card spread (`data-spread-shape="three-card"`) is
|
cells render in DOM so spread-switching never re-mutates the
|
||||||
active. Iter 3's SPREAD dropdown switches `data-spread-shape`
|
cross structure — per-spread visibility lives in SCSS via
|
||||||
on change to reveal these positions for Celtic Cross variants."""
|
`.my-sea-cross[data-spread="..."]` rules."""
|
||||||
picker = self._enter_picker_phase()
|
picker = self._enter_picker_phase()
|
||||||
for hidden_pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
|
visible = {".sea-pos-lay", ".sea-pos-cover", ".sea-pos-crown"}
|
||||||
with self.subTest(position=hidden_pos):
|
hidden = {".sea-pos-leave", ".sea-pos-loom", ".sea-pos-cross"}
|
||||||
elements = picker.find_elements(By.CSS_SELECTOR, hidden_pos)
|
for pos in visible | hidden:
|
||||||
|
with self.subTest(position=pos):
|
||||||
|
elements = picker.find_elements(By.CSS_SELECTOR, pos)
|
||||||
|
self.assertEqual(len(elements), 1, f"{pos} should render in DOM")
|
||||||
|
expected_visible = pos in visible
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(elements), 1,
|
elements[0].is_displayed(), expected_visible,
|
||||||
f"{hidden_pos} should render in DOM (gets shown on 6-card spread switch)",
|
f"{pos} visibility wrong for SAO default; expected {expected_visible}",
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
elements[0].is_displayed(),
|
|
||||||
f"{hidden_pos} should be hidden in default 3-card spread",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -495,16 +495,13 @@ class MySeaSpreadFormTest(FunctionalTest):
|
|||||||
def test_default_spread_is_situation_action_outcome(self):
|
def test_default_spread_is_situation_action_outcome(self):
|
||||||
"""Per the spec, `Situation, Action, Outcome` is the default
|
"""Per the spec, `Situation, Action, Outcome` is the default
|
||||||
spread on landing — selected in the combobox + reflected in
|
spread on landing — selected in the combobox + reflected in
|
||||||
the hidden `<input id="id_sea_spread">` initial value. The
|
the hidden `<input id="id_sea_spread">` initial value + on
|
||||||
combobox's `.sea-select-current` `<span>` carries the visible
|
`.my-sea-cross[data-spread]`."""
|
||||||
label (rendered even when the dropdown is closed)."""
|
|
||||||
picker = self._enter_picker_phase()
|
picker = self._enter_picker_phase()
|
||||||
hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread")
|
hidden = picker.find_element(By.CSS_SELECTOR, "#id_sea_spread")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
hidden.get_attribute("value"), "situation-action-outcome",
|
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(
|
selected = picker.find_element(
|
||||||
By.CSS_SELECTOR, ".sea-select-list [role='option'][aria-selected='true']"
|
By.CSS_SELECTOR, ".sea-select-list [role='option'][aria-selected='true']"
|
||||||
)
|
)
|
||||||
@@ -512,52 +509,132 @@ class MySeaSpreadFormTest(FunctionalTest):
|
|||||||
selected.get_attribute("textContent").strip(),
|
selected.get_attribute("textContent").strip(),
|
||||||
"Situation, Action, Outcome",
|
"Situation, Action, Outcome",
|
||||||
)
|
)
|
||||||
# The visible current-label span is always rendered.
|
|
||||||
current = picker.find_element(By.CSS_SELECTOR, ".sea-select-current")
|
current = picker.find_element(By.CSS_SELECTOR, ".sea-select-current")
|
||||||
self.assertEqual(current.text.strip(), "Situation, Action, Outcome")
|
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")
|
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
|
||||||
self.assertEqual(cross.get_attribute("data-spread-shape"), "three-card")
|
self.assertEqual(
|
||||||
|
cross.get_attribute("data-spread"), "situation-action-outcome",
|
||||||
|
)
|
||||||
|
|
||||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_picking_celtic_cross_reveals_six_card_positions(self):
|
def test_picking_spread_swaps_data_spread_and_position_visibility(self):
|
||||||
"""Clicking a 6-card spread option swaps `data-spread-shape`
|
"""Each spread reveals its own position subset (user-locked
|
||||||
to `six-card` on `.my-sea-cross` — crown / lay / cross become
|
2026-05-19):
|
||||||
visible. Three-card variants flip back to `three-card`."""
|
PPF → leave + cover + loom visible
|
||||||
|
SAO → lay + cover + crown
|
||||||
|
MBS → crown + lay + loom
|
||||||
|
DOS → loom + cross + cover
|
||||||
|
CC variants → all 6 surrounding positions.
|
||||||
|
`.my-sea-cross[data-spread]` swaps on combobox change; SCSS
|
||||||
|
rules toggle the inactive positions to `display: none`."""
|
||||||
|
ALL_POSITIONS = {"crown", "leave", "cover", "cross", "loom", "lay"}
|
||||||
|
SPREAD_POSITIONS = {
|
||||||
|
"past-present-future": {"leave", "cover", "loom"},
|
||||||
|
"situation-action-outcome": {"lay", "cover", "crown"},
|
||||||
|
"mind-body-spirit": {"crown", "lay", "loom"},
|
||||||
|
"desire-obstacle-solution": {"loom", "cross", "cover"},
|
||||||
|
"waite-smith": ALL_POSITIONS,
|
||||||
|
"escape-velocity": ALL_POSITIONS,
|
||||||
|
}
|
||||||
picker = self._enter_picker_phase()
|
picker = self._enter_picker_phase()
|
||||||
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
|
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 = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
|
|
||||||
combo.click()
|
def _pick(value):
|
||||||
waite_smith = picker.find_element(
|
# Combobox click outside an open dropdown opens it; click on
|
||||||
By.CSS_SELECTOR,
|
# an option inside selects + closes. Re-opening for each pick
|
||||||
".sea-select-list [role='option'][data-value='waite-smith']",
|
# keeps the test deterministic.
|
||||||
)
|
if combo.get_attribute("aria-expanded") != "true":
|
||||||
waite_smith.click()
|
combo.click()
|
||||||
self.wait_for(
|
opt = picker.find_element(
|
||||||
lambda: self.assertEqual(
|
By.CSS_SELECTOR,
|
||||||
cross.get_attribute("data-spread-shape"), "six-card"
|
f".sea-select-list [role='option'][data-value='{value}']",
|
||||||
)
|
)
|
||||||
)
|
opt.click()
|
||||||
for pos in (".sea-pos-crown", ".sea-pos-lay", ".sea-pos-cross"):
|
|
||||||
with self.subTest(position=pos):
|
for value, expected_visible in SPREAD_POSITIONS.items():
|
||||||
self.assertTrue(
|
with self.subTest(spread=value):
|
||||||
picker.find_element(By.CSS_SELECTOR, pos).is_displayed(),
|
_pick(value)
|
||||||
f"{pos} should be visible when 6-card spread selected",
|
self.wait_for(
|
||||||
|
lambda v=value: self.assertEqual(
|
||||||
|
cross.get_attribute("data-spread"), v
|
||||||
|
)
|
||||||
)
|
)
|
||||||
# Switch back to a three-card spread → hidden again.
|
for pos in ALL_POSITIONS:
|
||||||
combo.click()
|
element = picker.find_element(
|
||||||
mbs = picker.find_element(
|
By.CSS_SELECTOR, f".sea-pos-{pos}"
|
||||||
By.CSS_SELECTOR,
|
)
|
||||||
".sea-select-list [role='option'][data-value='mind-body-spirit']",
|
should_show = pos in expected_visible
|
||||||
|
self.assertEqual(
|
||||||
|
element.is_displayed(), should_show,
|
||||||
|
f"spread={value} pos={pos}: expected is_displayed={should_show}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Test 4 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_per_spread_position_labels_render_and_update(self):
|
||||||
|
"""Each visible empty slot carries a `.sea-pos-label` caption
|
||||||
|
whose text matches the spread's per-position label map (e.g.
|
||||||
|
SAO default: lay='Situation', cover='Action', crown='Outcome').
|
||||||
|
JS updates labels on spread change. Reappropriates the
|
||||||
|
GRAVITY/LEVITY (`.sea-stack-name`) caption styling."""
|
||||||
|
SPREAD_LABELS = {
|
||||||
|
"situation-action-outcome": {"lay": "Situation", "cover": "Action", "crown": "Outcome"},
|
||||||
|
"past-present-future": {"leave": "Past", "cover": "Present", "loom": "Future"},
|
||||||
|
"mind-body-spirit": {"crown": "Mind", "lay": "Body", "loom": "Spirit"},
|
||||||
|
"desire-obstacle-solution": {"loom": "Desire", "cross": "Obstacle","cover":"Solution"},
|
||||||
|
"waite-smith": {"crown": "Crown", "leave": "Beneath", "cover": "Cover",
|
||||||
|
"cross": "Cross", "loom": "Before", "lay": "Behind"},
|
||||||
|
}
|
||||||
|
picker = self._enter_picker_phase()
|
||||||
|
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
|
||||||
|
|
||||||
|
def _pick(value):
|
||||||
|
if combo.get_attribute("aria-expanded") != "true":
|
||||||
|
combo.click()
|
||||||
|
picker.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
f".sea-select-list [role='option'][data-value='{value}']",
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# SAO default — assert labels via the server-rendered initial state.
|
||||||
|
for pos, expected_label in SPREAD_LABELS["situation-action-outcome"].items():
|
||||||
|
with self.subTest(spread="situation-action-outcome", position=pos):
|
||||||
|
label_el = picker.find_element(
|
||||||
|
By.CSS_SELECTOR,
|
||||||
|
f".sea-pos-label[data-position='{pos}']",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
label_el.get_attribute("textContent").strip(),
|
||||||
|
expected_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Switch to each other spread + verify the labels update.
|
||||||
|
for spread, position_to_label in SPREAD_LABELS.items():
|
||||||
|
if spread == "situation-action-outcome":
|
||||||
|
continue
|
||||||
|
_pick(spread)
|
||||||
|
for pos, expected_label in position_to_label.items():
|
||||||
|
with self.subTest(spread=spread, position=pos):
|
||||||
|
label_el = self.wait_for(
|
||||||
|
lambda p=pos, lbl=expected_label: self._wait_label(p, lbl, picker)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
label_el.get_attribute("textContent").strip(),
|
||||||
|
expected_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _wait_label(self, position, expected_label, picker):
|
||||||
|
el = picker.find_element(
|
||||||
|
By.CSS_SELECTOR, f".sea-pos-label[data-position='{position}']"
|
||||||
)
|
)
|
||||||
mbs.click()
|
if el.get_attribute("textContent").strip() != expected_label:
|
||||||
self.wait_for(
|
raise AssertionError(
|
||||||
lambda: self.assertEqual(
|
f"label@{position}: got "
|
||||||
cross.get_attribute("data-spread-shape"), "three-card"
|
f"{el.get_attribute('textContent')!r}, want {expected_label!r}"
|
||||||
)
|
)
|
||||||
)
|
return el
|
||||||
|
|
||||||
# ── Test 4 ───────────────────────────────────────────────────────────────
|
# ── Test 4 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -285,25 +285,64 @@ body.page-gameboard {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .my-sea-cross renders all 6 surrounding positions (crown/lay/cross
|
// .my-sea-cross renders all 6 surrounding positions (crown/leave/lay/
|
||||||
// PLUS leave/loom/cover) so the SPREAD dropdown can toggle 3-card vs
|
// loom + cover/cross overlaid on core) unconditionally. The SPREAD
|
||||||
// 6-card layout via a single data-attribute swap (no DOM mutation).
|
// dropdown sets `data-spread="<name>"` on this element; per-spread
|
||||||
// Default `three-card` hides the 3 forsaken positions via `display:
|
// rules below hide the positions each spread doesn't use. Inherits
|
||||||
// none`; `six-card` (Celtic Cross variants) renders all 7 cells.
|
// the 3×3 `grid-template-areas` from _card-deck.scss line 1189-1200
|
||||||
// Cover always shows (it's part of both shapes — overlaid on sig).
|
// so visible cells land in their canonical positions; hidden cells
|
||||||
.my-sea-cross[data-spread-shape="three-card"] {
|
// just leave their grid slots empty.
|
||||||
grid-template-areas:
|
//
|
||||||
"leave core loom";
|
// Per-spread position subsets — user-locked 2026-05-19:
|
||||||
grid-template-rows: auto;
|
// PPF: leave (1) cover (2) loom (3) — horizontal middle row
|
||||||
|
// SAO: lay (1) cover (2) crown (3) — vertical center column
|
||||||
|
// MBS: crown (1) lay (2) loom (3) — T-shape (crown + lay vertical, loom right)
|
||||||
|
// DOS: loom (1) cross (2) cover (3) — sig-anchored cluster + loom
|
||||||
|
// CC variants: all 6 positions (Waite-Smith / Escape Velocity differ in DRAW ORDER only,
|
||||||
|
// not in position visibility).
|
||||||
|
|
||||||
|
.my-sea-cross[data-spread="past-present-future"] {
|
||||||
.sea-pos-crown,
|
.sea-pos-crown,
|
||||||
.sea-pos-lay,
|
.sea-pos-cross,
|
||||||
|
.sea-pos-lay { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-sea-cross[data-spread="situation-action-outcome"] {
|
||||||
|
.sea-pos-leave,
|
||||||
|
.sea-pos-loom,
|
||||||
.sea-pos-cross { display: none; }
|
.sea-pos-cross { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-sea-cross[data-spread-shape="six-card"] {
|
.my-sea-cross[data-spread="mind-body-spirit"] {
|
||||||
// Inherits `.sea-cross`'s 3×3 template from _card-deck.scss line 1189-
|
.sea-pos-leave,
|
||||||
// 1200; nothing to override layout-wise. All cells visible.
|
.sea-pos-cover,
|
||||||
|
.sea-pos-cross { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-sea-cross[data-spread="desire-obstacle-solution"] {
|
||||||
|
.sea-pos-leave,
|
||||||
|
.sea-pos-crown,
|
||||||
|
.sea-pos-lay { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Celtic Cross variants (waite-smith / escape-velocity) — all positions
|
||||||
|
// visible by default. No `display: none` overrides needed.
|
||||||
|
|
||||||
|
// Position-name caption inside each empty `.sea-card-slot--empty` —
|
||||||
|
// re-appropriates the GRAVITY/LEVITY `.sea-stack-name` typographic
|
||||||
|
// look (_card-deck.scss line 1557): small uppercase letter-spaced w.
|
||||||
|
// a subtle scaleY stretch, --terUser ink at 0.6 opacity. No polarity
|
||||||
|
// coloring — these are spread-position labels, not deck identifiers.
|
||||||
|
.sea-pos-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scaleY(1.2);
|
||||||
|
color: rgba(var(--terUser), 1);
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
// Section dividers inside the SPREAD combobox — labels "3-card spreads"
|
||||||
|
|||||||
@@ -66,21 +66,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Picker phase — flexible spread layout. Sig pins .sea-pos- #}
|
{# Picker phase — per-spread flexible layout. Sig pins .sea- #}
|
||||||
{# core; the 6 surrounding positions (crown/leave/loom/lay/ #}
|
{# pos-core; the 6 surrounding positions all render in DOM #}
|
||||||
{# cover/cross) all render unconditionally so the SPREAD #}
|
{# so the SPREAD dropdown can swap `.my-sea-cross[data- #}
|
||||||
{# dropdown can swap `.my-sea-cross[data-spread-shape]` #}
|
{# spread]` between the 4 three-card variants (each w. its #}
|
||||||
{# between `three-card` (default — hides crown/lay/cross via #}
|
{# own 3-position subset + draw order) + the 2 six-card #}
|
||||||
{# SCSS) + `six-card` (Celtic Cross variants — shows all). #}
|
{# Celtic Cross variants (all 6 surrounding positions). #}
|
||||||
{# Hidden until FREE DRAW click swaps data-phase to `picker`. #}
|
{# Each empty slot carries a `.sea-pos-label` caption (re- #}
|
||||||
|
{# appropriated from the GRAVITY/LEVITY .sea-stack-name look) #}
|
||||||
|
{# that JS updates per spread. #}
|
||||||
<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" data-spread-shape="three-card">
|
<div class="sea-cross my-sea-cross" data-spread="{{ default_spread }}">
|
||||||
<div class="sea-crucifix-cell sea-pos-crown">
|
<div class="sea-crucifix-cell sea-pos-crown">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty">
|
||||||
|
<span class="sea-pos-label" data-position="crown">Outcome</span>
|
||||||
|
</div>
|
||||||
</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">
|
||||||
|
<span class="sea-pos-label" data-position="leave"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-core">
|
<div class="sea-crucifix-cell sea-pos-core">
|
||||||
<div class="sig-stage-card sea-sig-card"
|
<div class="sig-stage-card sea-sig-card"
|
||||||
@@ -89,17 +95,25 @@
|
|||||||
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
|
<span class="sea-pos-label" data-position="cover">Action</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-pos-cross">
|
<div class="sea-pos-cross">
|
||||||
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing"></div>
|
<div class="sea-card-slot sea-card-slot--empty sea-card-slot--crossing">
|
||||||
|
<span class="sea-pos-label" data-position="cross"></span>
|
||||||
|
</div>
|
||||||
</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">
|
||||||
|
<span class="sea-pos-label" data-position="loom"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-lay">
|
<div class="sea-crucifix-cell sea-pos-lay">
|
||||||
<div class="sea-card-slot sea-card-slot--empty"></div>
|
<div class="sea-card-slot sea-card-slot--empty">
|
||||||
|
<span class="sea-pos-label" data-position="lay">Situation</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,20 +184,49 @@
|
|||||||
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
// Spread → cross-shape mapping. Celtic Cross variants
|
// Per-spread draw order + position labels — locked in spec
|
||||||
// (Waite-Smith / Escape Velocity) get the 6-card shape;
|
// (user 2026-05-19). Each three-card spread uses a DIFFERENT
|
||||||
// every other spread is 3-card (cover/leave/loom only).
|
// 3-position subset of the 6 surrounding positions, in a
|
||||||
var SIX_CARD_SPREADS = ['waite-smith', 'escape-velocity'];
|
// specific order. The Celtic Cross variants share position
|
||||||
|
// labels (Crown/Beneath/Cover/Cross/Before/Behind — gameroom
|
||||||
|
// vocabulary) but differ in draw order.
|
||||||
|
//
|
||||||
|
// DRAW_ORDER feeds iter 4's deck-click-deposit logic; for
|
||||||
|
// iter 3 it's just baked-in metadata.
|
||||||
|
var DRAW_ORDER = {
|
||||||
|
'past-present-future': ['leave', 'cover', 'loom'],
|
||||||
|
'situation-action-outcome': ['lay', 'cover', 'crown'],
|
||||||
|
'mind-body-spirit': ['crown', 'lay', 'loom'],
|
||||||
|
'desire-obstacle-solution': ['loom', 'cross', 'cover'],
|
||||||
|
'waite-smith': ['cover', 'cross', 'crown', 'lay', 'loom', 'leave'],
|
||||||
|
'escape-velocity': ['cover', 'cross', 'lay', 'leave', 'crown', 'loom'],
|
||||||
|
};
|
||||||
|
var POSITION_LABELS = {
|
||||||
|
'past-present-future': { leave: 'Past', cover: 'Present', loom: 'Future' },
|
||||||
|
'situation-action-outcome': { lay: 'Situation', cover: 'Action', crown: 'Outcome' },
|
||||||
|
'mind-body-spirit': { crown: 'Mind', lay: 'Body', loom: 'Spirit' },
|
||||||
|
'desire-obstacle-solution': { loom: 'Desire', cross: 'Obstacle',cover: 'Solution' },
|
||||||
|
'waite-smith': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
|
||||||
|
'escape-velocity': { crown: 'Crown', leave: 'Beneath', cover: 'Cover', cross: 'Cross', loom: 'Before', lay: 'Behind' },
|
||||||
|
};
|
||||||
var hidden = document.getElementById('id_sea_spread');
|
var hidden = document.getElementById('id_sea_spread');
|
||||||
var cross = document.querySelector('.my-sea-cross');
|
var cross = document.querySelector('.my-sea-cross');
|
||||||
if (!hidden || !cross) return;
|
if (!hidden || !cross) return;
|
||||||
|
function syncLabels(spread) {
|
||||||
|
var labels = POSITION_LABELS[spread] || {};
|
||||||
|
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
|
||||||
|
var pos = el.dataset.position;
|
||||||
|
el.textContent = labels[pos] || '';
|
||||||
|
});
|
||||||
|
}
|
||||||
function sync() {
|
function sync() {
|
||||||
var shape = SIX_CARD_SPREADS.indexOf(hidden.value) >= 0
|
cross.setAttribute('data-spread', hidden.value);
|
||||||
? 'six-card' : 'three-card';
|
syncLabels(hidden.value);
|
||||||
cross.setAttribute('data-spread-shape', shape);
|
|
||||||
}
|
}
|
||||||
hidden.addEventListener('change', sync);
|
hidden.addEventListener('change', sync);
|
||||||
sync();
|
sync();
|
||||||
|
// Exposed for iter 4 (deck-click-deposit reads DRAW_ORDER).
|
||||||
|
window._mySeaDrawOrder = DRAW_ORDER;
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user