- commit
- f92b270685d13109ab1bc38f2360b03c50ee6aba
- parent
- d99d0b0d494bf2d1f3c3a47d134d87fdf482c3c3
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 6 | +++++- |
| M | src/ | 10 | +++++++++- |
| M | src/ | 13 | ++++++++++++- |
| M | src/ | 22 | +++++++++++++++++++--- |
| M | tests/ | 1 | + |
| M | tests/ | 1 | + |
@@ -283,7 +283,11 @@ document.addEventListener('keydown', event => {
283283 const key = event.key.toLowerCase();284284 if (stage.scene === 'gift' && (key === 'i' || key === 'c')) {285285 const result = giftController.input(key);286- if (result.ok) { event.preventDefault(); stage.render(); }286+ if (result.ok) {287+ event.preventDefault();288+ if (result.action === 'details') stage.announce(giftController.accessibilityText());289+ stage.render();290+ }287291 } else if (stage.scene === 'workshop' && key === 'c' && workshopController.view().note) {288292 event.preventDefault();289293 Promise.resolve(workshopController.handleInput('copy')).finally(() => stage.render());@@ -88,6 +88,14 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
8888 const result = copyToClipboard(textToCopy);8989 return { ok: true, action: 'copy', text: textToCopy, copied: result };9090 }91+ function accessibilityText() {92+ if (!open || !preview) return 'Gift closed.';93+ const levels = levelList(preview);94+ const intro = `A pack for you. ${packName(preview)}. ${levels.length} ${levels.length === 1 ? 'proof' : 'proofs'}.`;95+ const notes = warnings.length ? ` ${warnings.length} ${warnings.length === 1 ? 'note' : 'notes'}: ${[...new Set(warnings.map(warning => warning.word))].join(', ')}. Press I for details or C to copy.` : '';96+ const details = detailsOpen ? ` ${warnings.slice(0, 5).map(warning => `${warning.levelTitle || warning.levelId || 'Proof'}: ${warning.message}`).join(' ')}${warnings.length > 5 ? ` And ${warnings.length - 5} more.` : ''}` : '';97+ return `${intro}${notes}${details} ${choices()[selected] === 'keep' ? 'Keep and play selected.' : `${choices()[selected]} selected.`}`;98+ }9199 function move(direction) { const values = choices(); if (direction === 'up' || direction === 'left') selected = (selected - 1 + values.length) % values.length; if (direction === 'down' || direction === 'right') selected = (selected + 1) % values.length; return values[selected]; }92100 function input(action) {93101 if (!open) return { ok: false, action: null };@@ -232,7 +240,7 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
232240 });233241 return { x: left, y: top, width: cardWidth, height: cardHeight, narrow, preview: { x: previewX, y: previewY, width: previewWidth, height: previewHeight }, actions: buttons.map(button => ({ ...button })), hintActions: hintButtons.map(button => ({ ...button })), hints: { x: hintStart, width: hintTotal }, hintY: hintBottom - rowHeight / 2, hintRows: hintRows.length, selected: selectedAction, now };234242 }235- const api = { show, hide, setPack, choose, input, hit, draw, get open() { return open; }, get mode() { return mode; }, get selected() { return choices()[selected]; }, get pack() { return preview ? cloneData(preview) : null; }, get collision() { return collides ? cloneData(collides) : null; }, get warnings() { return cloneData(warnings); }, get detailsOpen() { return detailsOpen; } };243+ const api = { show, hide, setPack, choose, input, hit, draw, accessibilityText, get open() { return open; }, get mode() { return mode; }, get selected() { return choices()[selected]; }, get pack() { return preview ? cloneData(preview) : null; }, get collision() { return collides ? cloneData(collides) : null; }, get warnings() { return cloneData(warnings); }, get detailsOpen() { return detailsOpen; } };236244 return Object.freeze(api);237245 }238246 @@ -80,6 +80,17 @@ export function createWorkshopScene(options = {}) {
8080 confirm: confirm ? { ...confirm, choices: [...confirm.choices] } : null,8181 };8282 }83+ function accessibilityText() {84+ const row = selectedRow();85+ const pack = row?.pack;86+ const selection = pack87+ ? `${pack.name || pack.manifest?.name || pack.id}. ${pack.levelCount} ${pack.levelCount === 1 ? 'proof' : 'proofs'}. ${pack.warnings.length} ${pack.warnings.length === 1 ? 'note' : 'notes'}. ${row.actions[actionIndex]} selected.`88+ : 'Import pack selected.';89+ const details = note90+ ? ` ${note.kind === 'warning' ? 'Pack notes' : 'Pack errors'}. ${note.visible.join(' ')}${note.remaining ? ` And ${note.remaining} more.` : ''} Press C to copy details or Escape to close.`91+ : '';92+ return `Workshop. ${selection}${details}`;93+ }8394 function setFeedback(message, kind = 'info') { feedback = { message, kind }; note = null; return view(); }8495 function showErrors(errors) {8596 const entries = Array.isArray(errors) ? errors : [errors];@@ -308,7 +319,7 @@ export function createWorkshopScene(options = {}) {
308319 309320 return Object.freeze({310321 id: 'workshop', eyebrow: 'WORKSHOP', title: "Packs of proofs you've added", prompt: 'IMPORT | PLAY | SHARE | EXPORT', help: 'ARROWS CHOOSE ENTER SELECT ESC BACK',311- draw, view, refresh, move, handleInput, hit, activate, copyDetails, showErrors, showWarnings,322+ draw, view, accessibilityText, refresh, move, handleInput, hit, activate, copyDetails, showErrors, showWarnings,312323 get selectedRow() { return rowIndex; }, get selectedAction() { return actionIndex; },313324 get hitLayout() { return hitLayout ? cloneData(hitLayout) : null; },314325 });@@ -167,6 +167,8 @@ export function createStage(options = {}) {
167167 h = innerHeight;168168 ctx.fillStyle = "#0F0E0D";169169 ctx.fillRect(0, 0, w, h);170+ if (scene !== "proof" && scene !== "gift" && scene !== "workshop")171+ canvas.setAttribute("aria-label", `Miau Is True ${scene}`);170172 if (scene === "title" && title) {171173 const drawn = title.draw(ctx, {172174 width: w,@@ -186,10 +188,12 @@ export function createStage(options = {}) {
186188 }187189 if (scene === "gift" && gift) {188190 gift.draw(ctx, performance.now(), { width: w, height: h });191+ canvas.setAttribute("aria-label", gift.accessibilityText?.() || "Miau Is True Gift");189192 return;190193 }191194 if (scene === "workshop" && workshop) {192195 workshop.draw(ctx, { width: w, height: h });196+ canvas.setAttribute("aria-label", workshop.accessibilityText?.() || "Miau Is True Workshop");193197 return;194198 }195199 if (scene === "proof" && boardState) {@@ -291,7 +295,9 @@ export function createStage(options = {}) {
291295 choices.hidden = true;292296 root.hidden = true;293297 root.dataset.scene = id;294- announce(`${spec.eyebrow || ""} ${titleEl.textContent}`.trim());298+ announce(id === "gift" && gift?.accessibilityText ? gift.accessibilityText()299+ : id === "workshop" && workshop?.accessibilityText ? workshop.accessibilityText()300+ : `${spec.eyebrow || ""} ${titleEl.textContent}`.trim());295301 render();296302 if (!reducedMotion && (id === "title" || id === "win" || id === "index"))297303 requestAnimationFrame(animateVisuals);@@ -401,9 +407,12 @@ export function createStage(options = {}) {
401407 return false;402408 }403409 if (scene === "workshop" && workshop) {404- workshop.handleInput(410+ const outcome = workshop.handleInput(405411 type === "confirm" ? "enter" : type === "menu" ? "escape" : intent.dir || type,406412 );413+ Promise.resolve(outcome).then(result => {414+ if (result?.event?.type === "notes") announce(workshop.accessibilityText?.());415+ });407416 render();408417 return true;409418 }@@ -412,6 +421,7 @@ export function createStage(options = {}) {
412421 type === "confirm" ? "confirm" : type === "menu" ? "escape" : intent.dir || type,413422 );414423 if (result.action === "later") closeGift();424+ if (result.action === "details") announce(gift.accessibilityText?.());415425 render();416426 return true;417427 }@@ -603,8 +613,14 @@ export function createStage(options = {}) {
603613 if (scene === "gift") {604614 const result = gift?.hit(x, y);605615 if (result?.action === "later") closeGift();616+ if (result?.action === "details") announce(gift.accessibilityText?.());617+ }618+ if (scene === "workshop") {619+ const outcome = workshop?.hit?.(x, y);620+ Promise.resolve(outcome).then(result => {621+ if (result?.event?.type === "notes") announce(workshop.accessibilityText?.());622+ });606623 }607- if (scene === "workshop") workshop?.hit?.(x, y);608624 if (609625 scene === "pause" &&610626 x >= innerWidth / 2 - 160 &&@@ -122,6 +122,7 @@ test('Gift keeps warning notes nonblocking, expands player details, and copies t
122122 assert.equal(scene.detailsOpen, false);123123 assert.deepEqual(scene.input('i'), { ok: true, action: 'details', open: true });124124 assert.equal(scene.detailsOpen, true);125+ assert.match(scene.accessibilityText(), /Read the board: SHUT has no behaviour/);125126 assert.deepEqual(scene.input('c'), { ok: true, action: 'copy', text: 'SHUT | intro | DOOR IS SHUT | levels[0].objects[3]', copied: undefined });126127 assert.equal(copied, 'SHUT | intro | DOOR IS SHUT | levels[0].objects[3]');127128 assert.equal(scene.input('enter').action, 'keep');@@ -112,6 +112,7 @@ test('warning notes add an amber Workshop chip and NOTES action', async () => {
112112 const result = await scene.handleInput('enter');113113 assert.equal(result.event.type, 'notes');114114 assert.equal(scene.view().note.kind, 'warning');115+ assert.match(scene.accessibilityText(), /Pack notes.*SHUT is not a rule word here/);115116 assert.deepEqual(scene.view().note.visible, [pack.warnings[0].message, pack.warnings[1].message]);116117 scene.draw(canvasContext(), { width: 800, height: 600 });117118 const labels = scene.hitLayout.rows.flatMap(row => row.actions.map(action => action.action));