feat: hand recorder v1 (tap-to-build, overlay) — recorder steps 2-4

Tap-to-build hand recorder per docs/RECORDER.md. Correctness by construction: every
tap writes a known value into a known slot, emitting the canonical structured contract
(server normalize_structured() is the shape authority, so the client is best-effort).

- recorder.js: mount-agnostic module (mounts into any container -> overlay now,
  standalone later, zero logic change). Pure buildStructured(state)->contract dict.
  Card picker: sticky-suit-or-rank-first + x (unknown suit) + unknown-card; lock resets
  per slot so it never bleeds between cards. Pre-fills from /session/data (stakes->blinds,
  stack->hero, villains->seated). Per-street action entry, manual street advance.
- recorder.css: full-screen overlay using app theme tokens; floating launcher.
- index.html: overlay div + script + launcher (chat/session stays mounted underneath).

Validated end-to-end: JS core builds a hand (sticky/rainbow/unknown cards all correct)
-> POST /hands -> normalized -> /hand/{id} replay round-trips identically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 04:18:38 +00:00
parent f745ef43a1
commit 50bcb5533f
3 changed files with 665 additions and 0 deletions
+24
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<title>Lyra Core Chat</title>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="/recorder.css" />
<!-- PWA -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="mobile-web-app-capable" content="yes" />
@@ -1245,5 +1246,28 @@
});
</script>
<script src="/nav.js"></script>
<!-- Hand recorder (overlay; chat/session stays mounted underneath) -->
<div id="recorderOverlay" class="rec-overlay"></div>
<script src="/recorder.js"></script>
<script>
(function () {
var btn = document.createElement("button");
btn.id = "recLaunch";
btn.textContent = " Hand";
btn.title = "Record a hand";
document.body.appendChild(btn);
var overlay = document.getElementById("recorderOverlay");
function close() { overlay.classList.remove("open"); overlay.innerHTML = ""; }
btn.addEventListener("click", function () {
overlay.innerHTML = "";
overlay.classList.add("open");
var sid = window.currentSession;
window.Recorder.mount(overlay, {
onClose: close,
onSave: function (id) { close(); window.open("/hand/" + id, "_blank"); }
});
});
})();
</script>
</body>
</html>