feat: stage reordering, skip controls, quick build, and commander session cleanup

This commit is contained in:
matt 2025-10-14 16:09:58 -07:00
parent f6a6f72950
commit 9ab3835e2a
15 changed files with 1040 additions and 34 deletions

View file

@ -212,6 +212,7 @@
</div>
{% endif %}
{% endif %}
{% include "build/_new_deck_skip_controls.html" %}
<details style="margin-top:.5rem;">
<summary>Advanced options (ideals)</summary>
<div style="display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:.5rem; margin-top:.5rem;">
@ -222,15 +223,40 @@
{% endfor %}
</div>
</details>
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:1rem;">
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:space-between; margin-top:1rem;">
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
<button type="submit" class="btn-continue">Create</button>
<div style="display:flex; gap:.5rem;">
<button type="submit" name="quick_build" value="1" class="btn-continue" id="quick-build-btn" title="Build entire deck automatically without approval steps">Quick Build</button>
<button type="submit" class="btn-continue">Create</button>
</div>
</div>
</form>
</div>
</div>
<script>
// Quick Build confirmation for mobile devices
(function() {
var quickBuildBtn = document.getElementById('quick-build-btn');
if (quickBuildBtn) {
// Detect mobile/tablet device (true touch-primary devices)
// Check for touch AND small screen or mobile user agent
var isMobile = ('ontouchstart' in window) &&
(window.innerWidth <= 1024 || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent));
if (isMobile) {
quickBuildBtn.addEventListener('click', function(e) {
var confirmed = confirm('Quick Build will create a complete deck automatically without approval steps. Continue?');
if (!confirmed) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
}
}
})();
// Utility function for parsing card lists
function parseCardList(content) {
const newlineRegex = /\r?\n/;

View file

@ -0,0 +1,279 @@
{# M3: Skip Controls UI - Collapsible section with 16+ checkboxes #}
<details id="skip-controls-section" style="margin-top:.75rem;">
<summary style="cursor:pointer; font-weight:500; user-select:none;">⏭️ Skip Build Steps <span class="muted" style="font-weight:400; font-size:12px;">(optional)</span></summary>
<div style="margin-top:.75rem; padding:.75rem; border:1px solid var(--border); border-radius:8px;">
<div class="muted" style="font-size:12px; margin-bottom:.75rem;">
Skip specific build stages to speed up deck creation. The builder will auto-fill skipped stages with optimal choices.
</div>
{# Land Controls #}
<div style="margin-bottom:1rem;">
<div style="font-weight:500; margin-bottom:.5rem; font-size:13px;">🏔️ Land Stages</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:.5rem; margin-left:1rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip all land selection stages">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_lands" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip all lands</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip to misc/utility lands (skip basics, staples, kindred, fetches, duals, triomes)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_to_misc" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip to misc lands</span>
</label>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:.5rem; margin-left:2rem; margin-top:.5rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip basic lands (step 1)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_basics" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip basics</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip staple lands (step 2)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_staples" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip staples</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip kindred/tribal lands (step 3)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_kindred" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip kindred</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip fetch lands (step 4)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_fetches" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip fetches</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip dual lands - shocks, typed duals (step 5)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_duals" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip duals</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip triome/tri-color lands (step 6)">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_triomes" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip triomes</span>
</label>
</div>
</div>
{# Creature Controls #}
<div style="margin-bottom:1rem;">
<div style="font-weight:500; margin-bottom:.5rem; font-size:13px;">🦁 Creature Stages</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:.5rem; margin-left:1rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip all creature selection stages">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_all_creatures" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip all creatures</span>
</label>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:.5rem; margin-left:2rem; margin-top:.5rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip primary theme creatures">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_creature_primary" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip primary creatures</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip secondary theme creatures">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_creature_secondary" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip secondary creatures</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip creature fill stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_creature_fill" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip creature fill</span>
</label>
</div>
</div>
{# Spell Controls #}
<div style="margin-bottom:1rem;">
<div style="font-weight:500; margin-bottom:.5rem; font-size:13px;">⚡ Spell Stages</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:.5rem; margin-left:1rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip all spell selection stages">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_all_spells" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip all spells</span>
</label>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:.5rem; margin-left:2rem; margin-top:.5rem;">
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip ramp spells stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_ramp" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip ramp</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip removal spells stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_removal" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip removal</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip board wipes stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_wipes" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip board wipes</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip card advantage stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_card_advantage" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip card advantage</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip protection spells stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_protection" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip protection</span>
</label>
<label class="skip-control-label" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; cursor:pointer;" title="Skip spell fill stage">
<input type="checkbox" class="skip-checkbox" data-skip-key="skip_spell_fill" style="margin:0; cursor:pointer;" />
<span style="font-size:12px;">Skip spell fill</span>
</label>
</div>
</div>
{# Post-Adjust Control (read-only, auto-managed) #}
<div>
<div class="muted" style="font-size:11px; font-style:italic; padding:.5rem; background:rgba(107, 114, 128, 0.1); border-radius:6px;">
💡 Post-adjustment will be automatically skipped when any stages are skipped above.
</div>
</div>
</div>
</details>
<script>
// M3: Skip Controls - HTMX toggle with mutual exclusivity
(function() {
const skipControls = document.getElementById('skip-controls-section');
if (!skipControls) return;
const checkboxes = skipControls.querySelectorAll('.skip-checkbox');
checkboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
const skipKey = this.getAttribute('data-skip-key');
const enabled = this.checked ? '1' : '0';
// Send toggle request to backend
fetch('/build/new/toggle-skip', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'skip_key=' + encodeURIComponent(skipKey) + '&enabled=' + encodeURIComponent(enabled)
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.error) {
console.error('Skip toggle error:', data.error);
// Revert checkbox state on error
checkbox.checked = !checkbox.checked;
return;
}
// Apply mutual exclusivity UI updates
applyMutualExclusivity();
})
.catch(function(error) {
console.error('Skip toggle failed:', error);
// Revert checkbox state on error
checkbox.checked = !checkbox.checked;
});
});
});
function applyMutualExclusivity() {
const landGroupFlags = ['skip_lands', 'skip_to_misc'];
const individualLandFlags = ['skip_basics', 'skip_staples', 'skip_kindred', 'skip_fetches', 'skip_duals', 'skip_triomes'];
const creatureGroupFlags = ['skip_all_creatures'];
const creatureSpecificFlags = ['skip_creature_primary', 'skip_creature_secondary', 'skip_creature_fill'];
const spellGroupFlags = ['skip_all_spells'];
const spellSpecificFlags = ['skip_ramp', 'skip_removal', 'skip_wipes', 'skip_card_advantage', 'skip_protection', 'skip_spell_fill'];
// Check which flags are enabled
const enabledFlags = {};
checkboxes.forEach(function(cb) {
const key = cb.getAttribute('data-skip-key');
enabledFlags[key] = cb.checked;
});
// Rule 1: If land group flags enabled, disable individual land flags
const landGroupEnabled = landGroupFlags.some(function(key) { return enabledFlags[key]; });
individualLandFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (landGroupEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
// Rule 2: If individual land flags enabled, disable land group flags
const individualLandEnabled = individualLandFlags.some(function(key) { return enabledFlags[key]; });
landGroupFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (individualLandEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
// Rule 3: If creature group enabled, disable specific creature flags
const creatureGroupEnabled = creatureGroupFlags.some(function(key) { return enabledFlags[key]; });
creatureSpecificFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (creatureGroupEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
// Rule 4: If specific creature flags enabled, disable creature group
const creatureSpecificEnabled = creatureSpecificFlags.some(function(key) { return enabledFlags[key]; });
creatureGroupFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (creatureSpecificEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
// Rule 5: If spell group enabled, disable specific spell flags
const spellGroupEnabled = spellGroupFlags.some(function(key) { return enabledFlags[key]; });
spellSpecificFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (spellGroupEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
// Rule 6: If specific spell flags enabled, disable spell group
const spellSpecificEnabled = spellSpecificFlags.some(function(key) { return enabledFlags[key]; });
spellGroupFlags.forEach(function(key) {
const cb = skipControls.querySelector('[data-skip-key="' + key + '"]');
if (cb) {
const label = cb.closest('.skip-control-label');
if (spellSpecificEnabled) {
cb.disabled = true;
if (label) label.style.opacity = '0.5';
} else {
cb.disabled = false;
if (label) label.style.opacity = '1';
}
}
});
}
// Apply initial state on load
applyMutualExclusivity();
})();
</script>

View file

@ -0,0 +1,16 @@
{# Quick Build Progress Indicator - Current Stage + Completed List #}
<div id="wizard" class="wizard-container" style="max-width:800px; margin:2rem auto; padding:1rem;">
<div id="wizard-content">
{% include "build/_quick_build_progress_content.html" %}
</div>
{# Auto-polling with HTMX - updates content while running, stops when Step 5 returned #}
<div id="quick-build-poller"
hx-get="/build/quick-progress"
hx-trigger="every 500ms"
hx-target="#wizard-content"
hx-swap="innerHTML"
style="display:none;">
</div>
</div>

View file

@ -0,0 +1,15 @@
{# Quick Build Progress Content (inner content only, for HTMX updates) #}
<div class="wizard-header" style="text-align:center; margin-bottom:3rem;">
<h2 style="margin:0 0 .5rem 0;">Automatic Build in Progress</h2>
<p class="muted" style="margin:0;">Building your deck automatically without approval steps...</p>
</div>
{# Simplified Phase Indicator #}
<div style="text-align:center; margin:4rem 0; padding:2rem; background:rgba(59,130,246,0.1); border:2px solid rgba(59,130,246,0.3); border-radius:8px;">
<div style="font-size:12px; text-transform:uppercase; letter-spacing:0.05em; color:#94a3b8; margin-bottom:0.75rem;">Current Phase</div>
<div style="font-size:24px; font-weight:600; color:#3b82f6;">{{ current_stage }}</div>
</div>
<div class="muted" style="text-align:center; font-size:12px; margin-top:2rem;">
<p style="margin:0;">This may take 10-30 seconds depending on deck complexity...</p>
</div>