Bag: hint and Clear selection stay visible under the list (t20)

The hint and Clear button were inside the scrolling bag list, so a long
Ctrl list scrolled them away. They now sit in a footer under the list, and
Clear is a full-width, highlighted "Clear selection" button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 14:29:02 -04:00
co-authored by Claude Opus 5.5
parent d643718c48
commit 196ce5ce62
6 changed files with 31 additions and 13 deletions
+1 -1
View File
@@ -404,7 +404,7 @@ The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
### src/ui/selection.js (242 lines) ### src/ui/selection.js (243 lines)
The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel. The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
+8
View File
@@ -4,6 +4,14 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-24: t20 follow-up: bag hint and Clear stay visible (Claude Code)
- The hint and Clear were inside the scrolling `#baglist`, so a long Ctrl list scrolled them away.
They now render into a new `#bagfoot` under the list (`src/shell.html`, `renderBag`); Clear is a
full-width, highlighted "Clear selection" button (`style.css`).
- `test/play.py`: a long Ctrl list scrolls while the hint and a full-width Clear stay visible.
Screenshots `shots/t20c-ctrl{,-900}.png`. Tests: `run_all.py --browser` all passed (play.py 232).
## 2026-10-24: t20 follow-ups: deselect when blown out, Ctrl + wheel, Ctrl+click (Claude Code) ## 2026-10-24: t20 follow-ups: deselect when blown out, Ctrl + wheel, Ctrl+click (Claude Code)
- Blowing the last of a selected material removes it from the selection (`toolTick`); materials - Blowing the last of a selected material removes it from the selection (`toolTick`); materials
+1
View File
@@ -25,6 +25,7 @@
</div> </div>
<div class="bar" id="bagbar"><i></i></div> <div class="bar" id="bagbar"><i></i></div>
<div id="baglist"></div> <div id="baglist"></div>
<div id="bagfoot"></div>
</div> </div>
</div> </div>
<div class="corner bl"> <div class="corner bl">
+5 -2
View File
@@ -180,11 +180,14 @@ table.mt td.dip { color: var(--melt); }
#baglist .tag { flex: none; padding: 0 3px; color: var(--patina); font-size: 11px; } #baglist .tag { flex: none; padding: 0 3px; color: var(--patina); font-size: 11px; }
#selcard .vacrow { flex-wrap: wrap; gap: 4px; } #selcard .vacrow { flex-wrap: wrap; gap: 4px; }
#selcard .vacrow .sw { width: 9px; height: 9px; } #selcard .vacrow .sw { width: 9px; height: 9px; }
#baglist .baghint { color: var(--ash-dim); font-size: 0.82em; padding: 4px 4px 0; } #bagfoot:empty { display: none; }
#bagfoot { border-top: 1px solid var(--soot-3); margin-top: 4px; }
#bagfoot .baghint { color: var(--ash-dim); font-size: 0.82em; padding: 4px 4px 0; }
.bagsep { margin: 6px 4px 2px; padding-top: 5px; border-top: 1px solid var(--ash-dim); color: var(--ash-dim); font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.08em; } .bagsep { margin: 6px 4px 2px; padding-top: 5px; border-top: 1px solid var(--ash-dim); color: var(--ash-dim); font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.08em; }
#baglist .bagrow.none { opacity: 0.55; } #baglist .bagrow.none { opacity: 0.55; }
#baglist .bagrow.none:hover { opacity: 1; } #baglist .bagrow.none:hover { opacity: 1; }
#baglist .bagclear { display: inline-flex; margin-left: 5px; padding: 1px 5px; background: transparent; color: var(--ash-dim); border: 1px solid var(--soot-3); font-size: inherit; } #bagfoot .bagclear { display: block; width: 100%; margin-top: 6px; padding: 4px 8px; background: rgba(92, 186, 166, 0.12); color: var(--bone); border: 1px solid var(--patina-dim); border-radius: 3px; font-weight: 600; cursor: pointer; }
#bagfoot .bagclear:hover { background: rgba(92, 186, 166, 0.24); border-color: var(--patina); }
#bagtoggle { background: none; border: none; color: var(--ash); cursor: pointer; padding: 0 2px; } #bagtoggle { background: none; border: none; color: var(--ash); cursor: pointer; padding: 0 2px; }
#info { padding: 7px 10px; min-width: calc(220px * var(--ui)); max-width: min(360px, 60vw); display: flex; flex-direction: column; gap: 2px; font-size: 0.92em; } #info { padding: 7px 10px; min-width: calc(220px * var(--ui)); max-width: min(360px, 60vw); display: flex; flex-direction: column; gap: 2px; font-size: 0.92em; }
+10 -9
View File
@@ -210,6 +210,16 @@ function renderBag() {
if (key === lastBagKey) return; if (key === lastBagKey) return;
lastBagKey = key; lastBagKey = key;
const box = $('baglist'); box.innerHTML = ''; const box = $('baglist'); box.innerHTML = '';
// the hint and Clear sit under the list, outside its scroll, so they never scroll away
const foot = $('bagfoot'); foot.innerHTML = '';
if (t === 'vacuum') {
foot.appendChild(el('div', { class: 'baghint' }, "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next"));
if (GAME.blow.size) {
const clear = el('button', { class: 'bagclear', 'data-bag-clear': '' }, 'Clear selection');
clear.onclick = () => { GAME.blow.clear(); lastBagKey = ''; hudDirty = true; playSfx('ui'); };
foot.appendChild(clear);
}
}
if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Vacuum.')); return; } if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Vacuum.')); return; }
for (const m of l) { for (const m of l) {
if (m === missing[0]) box.appendChild(el('div', { class: 'bagsep' }, 'Not in bag')); if (m === missing[0]) box.appendChild(el('div', { class: 'bagsep' }, 'Not in bag'));
@@ -230,13 +240,4 @@ function renderBag() {
row.appendChild(b); row.appendChild(b);
box.appendChild(row); box.appendChild(row);
} }
if (t === 'vacuum') {
const hint = el('div', { class: 'baghint' }, "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next");
if (GAME.blow.size) {
const clear = el('button', { class: 'bagclear', 'data-bag-clear': '' }, 'Clear');
clear.onclick = () => { GAME.blow.clear(); lastBagKey = ''; hudDirty = true; playSfx('ui'); };
hint.appendChild(clear);
}
box.appendChild(hint);
}
} }
+6 -1
View File
@@ -368,7 +368,7 @@ with sync_playwright() as p:
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80) pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80)
check('right click on a bag row leaves the selection alone', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)')) check('right click on a bag row leaves the selection alone', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
check('bag rows have no pickup tag', '⬇' not in pg.inner_text('#baglist')) check('bag rows have no pickup tag', '⬇' not in pg.inner_text('#baglist'))
check('bag hint describes the selection', "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next" in pg.locator('#baglist .baghint').inner_text()) check('bag hint describes the selection', "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next" in pg.locator('#bagfoot .baghint').inner_text())
ore_row.click(modifiers=['Control']); settle(80) ore_row.click(modifiers=['Control']); settle(80)
check('vacuum bag has no pick or blow buttons', pg.locator('#baglist [data-pick], #baglist [data-blow]').count() == 0) check('vacuum bag has no pick or blow buttons', pg.locator('#baglist [data-pick], #baglist [data-blow]').count() == 0)
pg.mouse.move(mx_, my_); pg.mouse.down(button='right'); settle(500); pg.mouse.up(button='right'); settle(600) pg.mouse.move(mx_, my_); pg.mouse.down(button='right'); settle(500); pg.mouse.up(button='right'); settle(600)
@@ -401,6 +401,11 @@ with sync_playwright() as p:
check('Meta release hides unselected missing materials', ev('!GAME.ctrl')) check('Meta release hides unselected missing materials', ev('!GAME.ctrl'))
ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); window.dispatchEvent(new Event('blur')); renderBag()") ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); window.dispatchEvent(new Event('blur')); renderBag()")
check('blur clears the modifier', ev('!GAME.ctrl')) check('blur clears the modifier', ev('!GAME.ctrl'))
r = ev("""(() => { const old = seen.slice(), sel = GAME.blow; for (const m of BAG_ORDER) seen[m] = 1; GAME.blow = new Set([COAL]); GAME.ctrl = true; lastBagKey = ''; renderBag();
const b = $('baglist'), f = $('bagfoot'), h = f.querySelector('.baghint'), c = f.querySelector('.bagclear'), fr = f.getBoundingClientRect(), br = b.getBoundingClientRect();
const out = [b.scrollHeight > b.clientHeight, !b.contains(h) && !b.contains(c), fr.top >= br.bottom - 1 && fr.bottom <= innerHeight, c.getBoundingClientRect().width >= f.clientWidth - 2, c.textContent];
seen.set(old); GAME.blow = sel; GAME.ctrl = false; lastBagKey = ''; renderBag(); return out; })()""")
check('a long Ctrl list scrolls while the hint and a full-width Clear stay visible below it', r[0] and r[1] and r[2] and r[3] and r[4] == 'Clear selection', r)
# Ctrl + wheel over the bag scrolls the list instead of zooming the page # Ctrl + wheel over the bag scrolls the list instead of zooming the page
r = ev("""(() => { const b = $('baglist'), mh = b.style.maxHeight; b.style.maxHeight = '40px'; b.scrollTop = 0; r = ev("""(() => { const b = $('baglist'), mh = b.style.maxHeight; b.style.maxHeight = '40px'; b.scrollTop = 0;
const w = new WheelEvent('wheel', { deltaY: 30, ctrlKey: true, bubbles: true, cancelable: true }); b.querySelector('.pick').dispatchEvent(w); const w = new WheelEvent('wheel', { deltaY: 30, ctrlKey: true, bubbles: true, cancelable: true }); b.querySelector('.pick').dispatchEvent(w);