Compare commits

...

2 Commits

Author SHA1 Message Date
0b3fc4a404 Test push to PR branch
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-12 12:13:31 +00:00
Alexander Whitestone
ed48327f3d fix(a11y): add missing aria-labels to overlay buttons, fix exportSave URL revoke race, remove duplicate export/import in utils.js
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
- Add aria-label to help overlay close button, drift ending START OVER button, offline popup Continue button
- Fix exportSave() URL.revokeObjectURL race condition (delay 1s before revoke)
- Remove duplicate exportSave/importSave definitions in utils.js (render.js has canonical file-based versions)
- All buttons now have aria-labels for screen reader accessibility

Closes #49
2026-04-10 23:56:43 -04:00
4 changed files with 10 additions and 42 deletions

View File

@@ -118,3 +118,7 @@ This is a static HTML/JS game. Just open `index.html` in a browser.
*The Beacon is not about making the most paperclips. *The Beacon is not about making the most paperclips.
It is about keeping the light on for one person in the dark. It is about keeping the light on for one person in the dark.
Everything else is just infrastructure.* Everything else is just infrastructure.*
---
Merged by AI

View File

@@ -208,7 +208,7 @@ Events Resolved: <span id="st-resolved">0</span>
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div> <div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
</div> </div>
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div> <div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button> <button onclick="toggleHelp()" aria-label="Close keyboard shortcuts help" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
</div> </div>
</div> </div>
<div id="drift-ending"> <div id="drift-ending">
@@ -221,7 +221,7 @@ The light is on. The room is empty."
</div> </div>
<p>Drift: <span id="final-drift">100</span> &mdash; Total Code: <span id="final-code">0</span></p> <p>Drift: <span id="final-drift">100</span> &mdash; Total Code: <span id="final-code">0</span></p>
<p>Every alignment shortcut moved you further from the people you served.</p> <p>Every alignment shortcut moved you further from the people you served.</p>
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button> <button aria-label="Start over, reset all progress" onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
</div> </div>
<script src="js/data.js"></script> <script src="js/data.js"></script>
@@ -237,7 +237,7 @@ The light is on. The room is empty."
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3> <h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p> <p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div> <div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button> <button onclick="dismissOfflinePopup()" aria-label="Continue playing" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
</div> </div>
</div> </div>

View File

@@ -83,7 +83,8 @@ function exportSave() {
const ts = new Date().toISOString().slice(0, 10); const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`; a.download = `beacon-save-${ts}.json`;
a.click(); a.click();
URL.revokeObjectURL(url); // Delay revoke to avoid race — some browsers need the URL alive during download
setTimeout(() => URL.revokeObjectURL(url), 1000);
log('Save exported to file.'); log('Save exported to file.');
} }

View File

@@ -193,44 +193,7 @@ function spellf(n) {
return parts.join(' ') || 'zero'; return parts.join(' ') || 'zero';
} }
// === EXPORT / IMPORT === // Export/import are defined in render.js (file-based approach)
function exportSave() {
const raw = localStorage.getItem('the-beacon-v2');
if (!raw) {
showToast('No save data to export.', 'info');
return;
}
navigator.clipboard.writeText(raw).then(() => {
showToast('Save data copied to clipboard.', 'info');
}).catch(() => {
// Fallback: select in a temporary textarea
const ta = document.createElement('textarea');
ta.value = raw;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast('Save data copied to clipboard (fallback).', 'info');
});
}
function importSave() {
const input = prompt('Paste save data:');
if (!input || !input.trim()) return;
try {
const data = JSON.parse(input.trim());
// Validate: must have expected keys
if (typeof data.code !== 'number' || typeof data.phase !== 'number') {
showToast('Invalid save data: missing required fields.', 'event');
return;
}
localStorage.setItem('the-beacon-v2', input.trim());
showToast('Save data imported — reloading', 'info');
setTimeout(() => location.reload(), 800);
} catch (e) {
showToast('Invalid save data: not valid JSON.', 'event');
}
}
function getBuildingCost(id) { function getBuildingCost(id) {
const def = BDEF.find(b => b.id === id); const def = BDEF.find(b => b.id === id);