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>
|
||||||
<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
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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