Sea Select options: OK beside the select + --priUser chunk rects — TDD

Restyle the spread-options page (post the scroll-snap refactor):
- OK `.btn-confirm` moves UP beside the `.sea-select` combobox (a new
  `.sea-select-row`), off the AUTO DRAW / DEL action row.
- OK gains `.btn-disabled` + × the moment the first card is drawn — inverse to
  DEL (which loses them then), simultaneous with the combobox locking. So
  `_chooseSpread` (OK) no longer locks; the lock + both btn states flip together
  at the first draw via `_setHasDrawn` + `_lockSpread`. Server-renders OK
  disabled/× when `saved_by_position`.
- The three chunks (spread/select/OK, the mini preview, AUTO DRAW/DEL) each get
  the same --priUser rounded rectangle as the GAME POST lines / composer
  (`_base.scss` `.form-control`): --priUser fill + half-alpha --secUser border +
  rounded + padding. The `.sea-form-col`/`-main` go transparent flex columns so
  the felt shows between the chunks.
- IT: OK enabled / DEL disabled when fresh; flips once a card is drawn.
  953 epic+gameboard ITs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-08 00:02:14 -04:00
parent edc9a49f06
commit 1fe257a7a9
3 changed files with 97 additions and 26 deletions

View File

@@ -4161,6 +4161,26 @@ class PickSeaUnifiedFeltTest(TestCase):
self.assertIn("AUTO", content)
self.assertNotIn("id_sea_lock_hand", content)
def test_ok_disabled_and_del_enabled_once_a_card_is_drawn(self):
"""Once a card is drawn (saved_by_position), the OK btn gains .btn-disabled
(the spread is locked) while DEL loses it — inverse states, simultaneous
with the select locking (user-spec 2026-06-07). Fresh (no draw): OK is
enabled, DEL disabled."""
import lxml.html
# Fresh — OK enabled, DEL disabled.
parsed = lxml.html.fromstring(self.client.get(self.url).content)
self.assertNotIn("btn-disabled", parsed.cssselect("#id_sea_confirm_spread")[0].get("class", ""))
self.assertIn("btn-disabled", parsed.cssselect("#id_sea_del")[0].get("class", ""))
# One card drawn — flips.
card_id = TarotCard.objects.filter(deck_variant=self.earthman).first().id
char = Character.objects.get(seat=self.pc_seat, confirmed_at__isnull=False)
char.celtic_cross = {"spread": "waite-smith", "hand": [
{"position": "cover", "card_id": card_id, "reversed": False, "polarity": "gravity"}]}
char.save(update_fields=["celtic_cross"])
parsed = lxml.html.fromstring(self.client.get(self.url).content)
self.assertIn("btn-disabled", parsed.cssselect("#id_sea_confirm_spread")[0].get("class", ""))
self.assertNotIn("btn-disabled", parsed.cssselect("#id_sea_del")[0].get("class", ""))
def test_only_two_celtic_cross_spread_options(self):
content = self.client.get(self.url).content.decode()
self.assertIn('data-value="waite-smith"', content)

View File

@@ -115,11 +115,51 @@ html.sea-open .sea-page.sea-page--room {
}
// The options form blends onto the --duoUser felt (transparent, content-sized) —
// "the modal contents on green felt", not the modal's purple --priUser card.
// "the modal contents on green felt". The form-col + form-main are transparent
// flex columns; each of the THREE chunks (spread/select/OK, preview, actions)
// is its own --priUser rounded rectangle (the GAME POST .form-control look).
.sea-page--room .sea-options-col .sea-form-col {
background: transparent;
width: auto;
min-width: 14rem;
min-width: 15rem;
max-width: 22rem;
padding: 0;
gap: 0.6rem;
}
.sea-page--room .sea-options-col .sea-form-main {
flex: 0 0 auto;
overflow: visible;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
// Combobox + OK on one row (OK to the RIGHT of the select).
.sea-page--room .sea-options-col .sea-select-row {
display: flex;
align-items: center;
gap: 0.5rem;
.sea-select { flex: 1; min-width: 0; }
}
// The three chunks — same --priUser rectangle as the GAME POST lines/composer
// (_base.scss .form-control): --priUser fill + a half-alpha --secUser border +
// rounded corners + padding. Resets each chunk's own modal-era margins/padding.
.sea-page--room .sea-options-col .sea-field,
.sea-page--room .sea-options-col .sea-cards-col--preview,
.sea-page--room .sea-options-col .sea-form-actions {
background-color: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.5);
border-radius: 0.5rem;
padding: 0.6rem 0.75rem;
margin: 0;
}
.sea-page--room .sea-options-col .sea-form-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
// Pre-confirm: only the options show (the cross is hidden), filling the aperture

View File

@@ -29,6 +29,10 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
{# Two Celtic Cross 6-card spreads only (user-spec 2026-06-07). #}
<input type="hidden" id="id_sea_spread" name="spread" autocomplete="off"
value="{{ sea_default_spread }}">
{# Combobox + OK on one row — OK confirms the spread → shunts. #}
{# OK gains .btn-disabled + × the moment the first card is drawn #}
{# (the spread is then locked, the select disables, DEL enables). #}
<div class="sea-select-row">
<div class="sea-select"
data-combobox
data-combobox-target="id_sea_spread"
@@ -49,6 +53,8 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
{% endif %}
</ul>
</div>
<button type="button" id="id_sea_confirm_spread" class="btn btn-confirm{% if saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}&times;{% else %}OK{% endif %}</button>
</div>
</div>
{# Miniaturized spread preview — shape only, never dealt to. Shows #}
@@ -57,9 +63,6 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
</div>
<div class="sea-form-actions">
{# OK confirms the spread → shunts the options down + reveals the #}
{# cross (a regular .btn-confirm, NOT a big .btn-primary). #}
<button type="button" id="id_sea_confirm_spread" class="btn btn-confirm">OK</button>
{# AUTO DRAW commits the remaining hand in one POST + animates onto #}
{# the cross (confirms the spread first if not yet). DEL clears. #}
<button type="button"
@@ -215,7 +218,9 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
if (_spreadChosen) return;
_spreadChosen = true;
page.classList.add('sea-spread-chosen');
_lockSpread();
// NB: the combobox is NOT locked here — the spread stays changeable (scroll
// up) until the FIRST card is drawn, when _lockSpread() + _setHasDrawn(true)
// disable the select + OK together (user-spec 2026-06-07).
_scrollToCross();
}
function _scrollToCross() {
@@ -234,10 +239,16 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
}
requestAnimationFrame(step);
}
if (okBtn) okBtn.addEventListener('click', _chooseSpread);
if (okBtn) okBtn.addEventListener('click', function () {
if (okBtn.classList.contains('btn-disabled')) return;
_chooseSpread();
});
// First card drawn → DEL un-disables ("DEL") + OK disables ("×"), simultaneous
// with the combobox locking (`_lockSpread`). The two btns are inverse states.
function _setHasDrawn(on) {
if (delBtn) { delBtn.classList.toggle('btn-disabled', !on); delBtn.innerHTML = on ? 'DEL' : '×'; }
if (okBtn) { okBtn.classList.toggle('btn-disabled', on); okBtn.innerHTML = on ? '×' : 'OK'; }
}
function _setComplete(on, live) {
_locked = on;