feat(mystery): theme toggle wiring with localStorage persistence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 00:14:15 -05:00
parent 5f5dc6071b
commit 3c0c386bbe
3 changed files with 87 additions and 41 deletions
+1
View File
@@ -43,6 +43,7 @@ import '../mystery/ui/crt.css'
</script> </script>
<script> <script>
import '../mystery/ui/terminal.ts' import '../mystery/ui/terminal.ts'
import '../mystery/ui/theme.ts'
</script> </script>
</body> </body>
</html> </html>
+44 -41
View File
@@ -23,6 +23,47 @@ if (!transcriptEl || !inputEl) {
state = initialStateFor(world) state = initialStateFor(world)
} }
const buildParserContext = (s: GameState): ParserContext => {
const room = world.rooms[s.location]
const visibleNouns: { id: string; aliases: string[] }[] = []
if (room) {
for (const id of room.items) {
const it = world.items[id]
if (it) visibleNouns.push({ id, aliases: it.names })
}
if (room.encounter && s.encounterState[room.encounter]) {
visibleNouns.push({ id: room.encounter, aliases: [room.encounter] })
}
}
for (const inst of s.inventory) {
const it = world.items[inst.id]
if (it) visibleNouns.push({ id: inst.id, aliases: it.names })
}
return {
knownItems: Object.keys(world.items),
knownEncounters: Object.keys(world.encounters),
visibleNouns,
inventoryItemIds: s.inventory.map((i) => i.id),
lastNoun: s.lastNoun,
awaitingDisambiguation: s.pendingDisambiguation,
}
}
const renderAll = (lines: TranscriptLine[]): void => {
if (!transcriptEl) return
for (const line of lines) {
const el = document.createElement('div')
el.className = line.kind
el.textContent = line.text
transcriptEl.appendChild(el)
}
transcriptEl.scrollTop = transcriptEl.scrollHeight
}
const appendLines = (lines: TranscriptLine[]): void => {
renderAll(lines)
}
renderAll(state.transcript) renderAll(state.transcript)
inputEl.focus() inputEl.focus()
@@ -76,6 +117,9 @@ if (!transcriptEl || !inputEl) {
appendLines(result.appended) appendLines(result.appended)
saveState(state) saveState(state)
transcriptEl.scrollTop = transcriptEl.scrollHeight transcriptEl.scrollTop = transcriptEl.scrollHeight
if (raw.trim().toLowerCase() === 'theme') {
document.dispatchEvent(new CustomEvent('halfstreet-toggle-theme'))
}
} catch (err) { } catch (err) {
console.error('[halfstreet] dispatch error', err) console.error('[halfstreet] dispatch error', err)
appendLines([{ kind: 'system', text: '[ The terminal hums and resets. ]' }]) appendLines([{ kind: 'system', text: '[ The terminal hums and resets. ]' }])
@@ -88,45 +132,4 @@ if (!transcriptEl || !inputEl) {
window.location.href = '/' window.location.href = '/'
} }
}) })
function buildParserContext(s: GameState): ParserContext {
const room = world.rooms[s.location]
const visibleNouns: { id: string; aliases: string[] }[] = []
if (room) {
for (const id of room.items) {
const it = world.items[id]
if (it) visibleNouns.push({ id, aliases: it.names })
}
if (room.encounter && s.encounterState[room.encounter]) {
visibleNouns.push({ id: room.encounter, aliases: [room.encounter] })
}
}
for (const inst of s.inventory) {
const it = world.items[inst.id]
if (it) visibleNouns.push({ id: inst.id, aliases: it.names })
}
return {
knownItems: Object.keys(world.items),
knownEncounters: Object.keys(world.encounters),
visibleNouns,
inventoryItemIds: s.inventory.map((i) => i.id),
lastNoun: s.lastNoun,
awaitingDisambiguation: s.pendingDisambiguation,
}
}
function renderAll(lines: TranscriptLine[]): void {
if (!transcriptEl) return
for (const line of lines) {
const el = document.createElement('div')
el.className = line.kind
el.textContent = line.text
transcriptEl.appendChild(el)
}
transcriptEl.scrollTop = transcriptEl.scrollHeight
}
function appendLines(lines: TranscriptLine[]): void {
renderAll(lines)
}
} }
+42
View File
@@ -0,0 +1,42 @@
const STORAGE_KEY = 'halfstreet:theme:v1'
type Theme = 'amber' | 'ansi'
function getStored(): Theme {
try {
return (localStorage.getItem(STORAGE_KEY) as Theme | null) === 'ansi' ? 'ansi' : 'amber'
} catch {
return 'amber'
}
}
function setTheme(theme: Theme): void {
document.documentElement.setAttribute('data-mystery-theme', theme)
try {
localStorage.setItem(STORAGE_KEY, theme)
} catch {
// ignore
}
for (const btn of document.querySelectorAll<HTMLButtonElement>('[data-theme-choice]')) {
btn.setAttribute('aria-pressed', btn.dataset['themeChoice'] === theme ? 'true' : 'false')
}
}
const initial = getStored()
setTheme(initial)
document.querySelectorAll<HTMLButtonElement>('[data-theme-choice]').forEach((btn) => {
btn.addEventListener('click', () => {
const next = (btn.dataset['themeChoice'] as Theme | undefined) ?? 'amber'
setTheme(next)
})
})
// Allow the engine's `theme` meta-command (handled in terminal.ts) to flip
// without going through the button by listening for a custom event.
document.addEventListener('halfstreet-toggle-theme', () => {
const current = (document.documentElement.getAttribute('data-mystery-theme') as Theme | null) ?? 'amber'
setTheme(current === 'amber' ? 'ansi' : 'amber')
})
export {}