clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

fix(workshop): fit pack actions on mobile screens

Dami committed 7bbe179
commit
7bbe179f926fd8e017b7497d522801bf08372484
parent
449765f3c4581a05b8258867fa1b7a5aba252910
author
Dami <dami.etoile@gmail.com>
2 files changed, +61 -19
Msrc/scenes/workshop.js57+++++++++++++++++++++---------
Mtests/workshop.test.js23++++++++++++-
M src/scenes/workshop.js +39 -18
@@ -177,7 +177,7 @@ export function createWorkshopScene(options = {}) {
177177 const target = hitLayout.rows.find(row => pointerY >= row.y && pointerY <= row.y + row.height);178178 if (!target) return view();179179 rowIndex = target.index;180- const action = target.actions.findIndex(cell => pointerX >= cell.x && pointerX <= cell.x + cell.width);180+ const action = target.actions.findIndex(cell => pointerX >= cell.x && pointerX <= cell.x + cell.width && pointerY >= cell.y && pointerY <= cell.y + cell.height);181181 if (action >= 0) { actionIndex = action; return activate(); }182182 if (target.type === 'pack') {183183 actionIndex = 0;
@@ -191,35 +191,56 @@ export function createWorkshopScene(options = {}) {
191191 const colors = { ...WORKSHOP_COLORS, ...(options.colors || {}) };192192 const width = dimensions.width || ctx.canvas?.width || 800;193193 const height = dimensions.height || ctx.canvas?.height || 600;194+ const compact = width < 600;194195 ctx.fillStyle = colors.stage; ctx.fillRect(0, 0, width, height);195- drawText(ctx, 'WORKSHOP', 48, 48, 13, colors.muted, 'left', 800);196- drawText(ctx, "packs of proofs you've added", 48, 86, 28, colors.fg, 'left', 850);197- let hintX = width - 48;198- for (const [key, verb] of [['ESC', 'back'], ['ENTER', 'select'], ['←→', 'action'], ['↑↓', 'choose']]) hintX = drawKeyHint(ctx, key, verb, hintX, 48, colors);199- const top = 130; const rowHeight = Math.max(62, Math.min(84, (height - top - 42) / Math.max(rows().length, 1)));196+ const margin = compact ? 24 : 48;197+ drawText(ctx, 'WORKSHOP', margin, compact ? 30 : 48, 13, colors.muted, 'left', 800);198+ if (compact) {199+ drawText(ctx, 'packs of proofs', margin, 76, 24, colors.fg, 'left', 850);200+ drawText(ctx, "you've added", margin, 105, 24, colors.fg, 'left', 850);201+ let hintX = width - margin;202+ for (const [key, verb] of [['ESC', 'back'], ['ENTER', 'select']]) hintX = drawKeyHint(ctx, key, verb, hintX, 145, colors);203+ hintX = width - margin;204+ for (const [key, verb] of [['←→', 'action'], ['↑↓', 'choose']]) hintX = drawKeyHint(ctx, key, verb, hintX, 173, colors);205+ } else {206+ drawText(ctx, "packs of proofs you've added", margin, 86, 28, colors.fg, 'left', 850);207+ let hintX = width - margin;208+ for (const [key, verb] of [['ESC', 'back'], ['ENTER', 'select'], ['←→', 'action'], ['↑↓', 'choose']]) hintX = drawKeyHint(ctx, key, verb, hintX, 48, colors);209+ }210+ const top = compact ? 204 : 130;211+ const rowHeight = compact ? 120 : Math.max(62, Math.min(84, (height - top - 42) / Math.max(rows().length, 1)));212+ const visibleCount = Math.max(1, Math.floor((height - top - 20) / rowHeight));213+ const firstRow = Math.min(Math.max(0, rowIndex - visibleCount + 1), Math.max(0, rows().length - visibleCount));200214 hitLayout = { width, height, rows: [], noteCopy: { x: width, y: height, height: 0 } };201215 rows().forEach((row, index) => {202- const y = top + index * rowHeight; const selected = index === rowIndex;203- const cells = row.type === 'pack' ? row.actions.map((action, actionNumber) => ({ action, x: width - 64 - (row.actions.length - actionNumber) * 92, y: y + rowHeight / 2 - 22, width: 84, height: 44 })) : [];216+ if (index < firstRow || index >= firstRow + visibleCount) return;217+ const y = top + (index - firstRow) * rowHeight; const selected = index === rowIndex;218+ const cardX = margin; const cardWidth = width - margin * 2;219+ const buttonGap = compact ? 7 : 8;220+ const buttonWidth = compact ? (cardWidth - 32 - buttonGap * (row.actions.length - 1)) / row.actions.length : 84;221+ const buttonY = compact ? y + 70 : y + rowHeight / 2 - 14;222+ const buttonX = actionNumber => compact ? cardX + 16 + actionNumber * (buttonWidth + buttonGap) : width - 64 - (row.actions.length - actionNumber) * 92;223+ const cells = row.type === 'pack' ? row.actions.map((action, actionNumber) => ({ action, x: buttonX(actionNumber), y: buttonY - (compact ? 4 : 8), width: buttonWidth, height: 44 })) : [];204224 hitLayout.rows.push({ index, id: row.id, type: row.type, y, height: rowHeight - 10, actions: cells });205- ctx.fillStyle = selected ? colors.YOU : colors.raised; ctx.fillRect(48, y, width - 96, rowHeight - 10);206- ctx.strokeStyle = selected ? colors.YOU : colors.edge; ctx.lineWidth = 1; ctx.strokeRect(48, y, width - 96, rowHeight - 10);207- drawText(ctx, row.type === 'import' ? '+' : String(index).padStart(2, '0'), 70, y + 26, 16, selected ? colors.ink : colors.muted, 'left', 850);225+ ctx.fillStyle = selected ? colors.YOU : colors.raised; ctx.fillRect(cardX, y, cardWidth, rowHeight - 10);226+ ctx.strokeStyle = selected ? colors.YOU : colors.edge; ctx.lineWidth = 1; ctx.strokeRect(cardX, y, cardWidth, rowHeight - 10);227+ drawText(ctx, row.type === 'import' ? '+' : String(index).padStart(2, '0'), compact ? cardX + 20 : 70, y + 26, 16, selected ? colors.ink : colors.muted, 'left', 850);208228 if (row.type === 'import') {209- drawText(ctx, row.label, 110, y + (rowHeight - 10) / 2, 16, selected ? colors.ink : colors.fg, 'left', 850);229+ drawText(ctx, row.label, compact ? cardX + 62 : 110, y + (rowHeight - 10) / 2, 16, selected ? colors.ink : colors.fg, 'left', 850);210230 } else {211231 const pack = row.pack; const name = pack.manifest?.name || pack.name || pack.id;212- ctx.font = fit(ctx, name, width * 0.38, 17, 850); ctx.fillStyle = selected ? colors.ink : colors.fg; ctx.textAlign = 'left'; ctx.fillText(name, 110, y + 22);213- drawText(ctx, `${pack.levelCount} ${pack.levelCount === 1 ? 'proof' : 'proofs'}${pack.author ? ` | by ${pack.author}` : ''}`, 110, y + 47, 10, selected ? colors.ink : colors.muted, 'left', 700);232+ const textX = compact ? cardX + 62 : 110;233+ ctx.font = fit(ctx, name, compact ? cardWidth - 82 : width * 0.38, 17, 850); ctx.fillStyle = selected ? colors.ink : colors.fg; ctx.textAlign = 'left'; ctx.fillText(name, textX, y + 27);234+ drawText(ctx, `${pack.levelCount} ${pack.levelCount === 1 ? 'proof' : 'proofs'}${pack.author ? ` | by ${pack.author}` : ''}`, textX, y + 51, 10, selected ? colors.ink : colors.muted, 'left', 700);214235 if (pack.forkOf) {215236 const sourcePack = packs.find(candidate => candidate.id === pack.forkOf);216237 const sourceName = pack.forkOfName || sourcePack?.manifest?.name || sourcePack?.name || pack.forkOf;217- drawText(ctx, `COPY OF ${sourceName}`, 110, y + rowHeight - 17, 9, selected ? colors.ink : colors.muted, 'left', 700);238+ drawText(ctx, `COPY OF ${sourceName}`, textX, compact ? y + 66 : y + rowHeight - 17, 9, selected ? colors.ink : colors.muted, 'left', 700);218239 }219240 row.actions.forEach((action, actionNumber) => {220- const actionX = width - 64 - (row.actions.length - actionNumber) * 92; const active = selected && actionNumber === actionIndex;221- roundRect(ctx, actionX, y + rowHeight / 2 - 14, 84, 28, 3); ctx.fillStyle = active ? colors.fg : colors.board; ctx.fill(); ctx.strokeStyle = active ? colors.ink : colors.edge; ctx.stroke();222- drawText(ctx, actionLabel[action], actionX + 42, y + rowHeight / 2, action === 'share' ? 8 : 9, active ? colors.ink : colors.fg, 'center', 800);241+ const actionX = buttonX(actionNumber); const active = selected && actionNumber === actionIndex;242+ roundRect(ctx, actionX, buttonY, buttonWidth, compact ? 36 : 28, 3); ctx.fillStyle = active ? colors.fg : colors.board; ctx.fill(); ctx.strokeStyle = active ? colors.ink : colors.edge; ctx.stroke();243+ drawText(ctx, actionLabel[action], actionX + buttonWidth / 2, buttonY + (compact ? 18 : 14), action === 'share' ? 8 : 9, active ? colors.ink : colors.fg, 'center', 800);223244 });224245 }225246 });
M tests/workshop.test.js +22 -1
@@ -120,7 +120,7 @@ test('hit selects a rendered row and dispatches its action cell', async () => {
120120 const scene = createWorkshopScene({ service: serviceWith([makePack('garden')]), onPlay: id => played.push(id) });121121 scene.draw(canvasContext(), { width: 800, height: 600 });122122 // The imported pack is row 1; its PLAY cell is the first action at x 368.123- const result = await scene.hit(380, 130 + 70 + 25);123+ const result = await scene.hit(380, 130 + 84 + 42);124124 assert.deepEqual(played, ['garden']);125125 assert.equal(result.event.type, 'play');126126 assert.equal(scene.view().selectedRow, 1);
@@ -139,6 +139,27 @@ test('tapping a pack row opens its actions and action hit targets are at least 4
139139 assert.ok(packRow.actions.every(action => action.height >= 44));140140 });141141 142+test('mobile Workshop keeps header and pack actions within the viewport while selection scrolls', () => {143+ const scene = createWorkshopScene({ packs: Array.from({ length: 6 }, (_, index) => makePack(`pack-${index}`)) });144+ const ctx = canvasContext();145+ scene.draw(ctx, { width: 390, height: 844 });146+ const labels = ctx.calls.filter(call => call[0] === 'fillText').map(call => call[1]);147+ assert.ok(labels.includes('packs of proofs'));148+ assert.ok(labels.includes("you've added"));149+ assert.ok(!labels.includes("packs of proofs you've added"));150+ for (const row of scene.hitLayout.rows) {151+ for (const action of row.actions) {152+ assert.ok(action.x >= 24 && action.x + action.width <= 366);153+ assert.ok(action.y >= row.y && action.y + action.height <= row.y + row.height);154+ assert.ok(action.height >= 44);155+ }156+ }157+ for (let index = 0; index < 5; index += 1) scene.handleInput('down');158+ scene.draw(canvasContext(), { width: 390, height: 844 });159+ assert.ok(scene.hitLayout.rows.some(row => row.index === scene.selectedRow));160+ assert.ok(scene.hitLayout.rows.every(row => row.y + row.height <= 844));161+});162+142163 test('IMPORT label is vertically centered in its row', () => {143164 const scene = createWorkshopScene({ packs: [makePack('garden')] });144165 const ctx = canvasContext();