feat(mystery): theme toggle wiring with localStorage persistence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import '../mystery/ui/crt.css'
|
||||
</script>
|
||||
<script>
|
||||
import '../mystery/ui/terminal.ts'
|
||||
import '../mystery/ui/theme.ts'
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+44
-41
@@ -23,6 +23,47 @@ if (!transcriptEl || !inputEl) {
|
||||
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)
|
||||
inputEl.focus()
|
||||
|
||||
@@ -76,6 +117,9 @@ if (!transcriptEl || !inputEl) {
|
||||
appendLines(result.appended)
|
||||
saveState(state)
|
||||
transcriptEl.scrollTop = transcriptEl.scrollHeight
|
||||
if (raw.trim().toLowerCase() === 'theme') {
|
||||
document.dispatchEvent(new CustomEvent('halfstreet-toggle-theme'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[halfstreet] dispatch error', err)
|
||||
appendLines([{ kind: 'system', text: '[ The terminal hums and resets. ]' }])
|
||||
@@ -88,45 +132,4 @@ if (!transcriptEl || !inputEl) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user