fix(ui): polish terminal options and assets
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-10 07:00:22 -05:00
parent 33933b00d7
commit daa5e9d655
13 changed files with 387 additions and 33 deletions
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<path d="m937.31 531.24-50.766-13.922c-8.8125-2.3906-15.938-8.8594-19.172-17.391-2.5312-6.7031-5.5312-13.312-8.8594-20.297h-0.046875c-4.125-7.8281-4.125-17.203 0-25.078l25.781-44.297 0.046875 0.046875c5.3906-9.1406 4.7344-20.625-1.6875-29.062-20.062-25.453-43.453-48.141-69.469-67.453-8.4375-5.7656-19.359-6.4688-28.453-1.7812l-45.938 25.078h-0.046875c-8.4844 4.0781-18.375 4.0781-26.859 0-7.9219-3.4688-14.766-6-20.625-8.1562-8.7188-2.8125-15.562-9.6562-18.375-18.375l-14.156-48.609c-2.625-10.359-11.344-18.047-21.984-19.312-12.188-1.5-24.422-2.3438-36.703-2.625-12.047 0.46875-24.047 1.5469-36 3.2344-10.688 1.0781-19.594 8.6719-22.312 19.078l-14.156 48.703v0.046875c-2.8594 8.7188-9.75 15.609-18.469 18.469-5.8594 2.1562-12.703 4.6875-20.766 8.2969v-0.046875c-8.4844 4.0312-18.328 4.0312-26.766 0l-46.312-25.781c-9.2344-5.1094-20.531-4.4531-29.062 1.6875-26.016 19.266-49.312 41.906-69.375 67.312-6.4219 8.4375-7.0781 19.922-1.6406 29.062l25.781 44.297v-0.046875c4.5938 7.875 4.875 17.578 0.70312 25.688-3.4688 7.2188-6.4688 13.781-9 20.531-3.2344 8.5312-10.359 14.953-19.172 17.391l-50.766 13.922c-10.219 2.5312-17.859 10.969-19.453 21.375-4.5 31.406-4.5 63.328 0 94.781 1.2656 10.641 9.0938 19.312 19.547 21.609l50.391 13.781h0.046875c8.8594 2.4844 16.078 8.9531 19.406 17.531 2.5312 6.9375 5.4844 13.688 8.7656 20.297 3.8438 7.9219 3.6094 17.25-0.70312 24.938l-25.922 44.625c-5.3906 9.1875-4.5938 20.672 1.9219 29.062 20.062 25.219 43.359 47.672 69.234 66.844 8.5781 6.2812 20.062 6.8906 29.297 1.5469l45.703-24.844c8.3906-4.1719 18.234-4.4531 26.859-0.70312 6.8438 3.1406 13.781 5.9531 20.906 8.3906 8.625 3 15.375 9.8906 18.234 18.609l14.156 48c2.7188 10.688 11.906 18.516 22.922 19.453 11.953 1.6406 23.953 2.5781 36 2.8594 12.047-0.23438 24.047-1.2188 36-2.8594 10.641-1.125 19.5-8.7188 22.219-19.078l14.156-48.844c2.8125-8.6719 9.5156-15.562 18.094-18.609 7.4531-2.625 14.156-5.2969 21-8.3906 8.6719-3.8438 18.562-3.6562 27 0.5625l45.984 25.219c9.0938 5.4844 20.578 4.9219 29.156-1.3125 26.016-19.172 49.406-41.766 69.469-67.078 6.6094-8.3438 7.3594-19.969 1.7812-29.062l-25.641-45c-4.4062-7.5938-4.7344-16.922-0.84375-24.844 3.4688-7.0781 6.375-13.781 8.7656-20.156 3.2344-8.6719 10.453-15.281 19.406-17.766l49.453-13.641c10.641-1.9688 18.891-10.453 20.531-21.141 4.5-31.547 4.5-63.609 0-95.156-1.5-10.406-9.0938-18.891-19.219-21.609zm-3.8438 113.77c-0.60938 0.89062-1.5469 1.5-2.625 1.6875l-50.531 13.312c-15.984 4.4531-28.922 16.219-34.922 31.688-2.2969 6.2812-4.9688 12.469-7.9219 18.469-7.1719 14.812-6.6094 32.25 1.5938 46.547l25.688 44.391-0.046875 0.046875c0.70312 0.9375 0.70312 2.25 0 3.2344-18.844 23.719-40.828 44.812-65.25 62.766h-3.9375l-45.844-24.938-0.046875-0.046875c-14.766-7.5469-32.203-8.0625-47.391-1.3125-6.375 2.9062-12.891 5.5312-19.547 7.7812-15.422 5.6719-27.375 18.188-32.297 33.844l-14.391 49.453c-0.28125 1.2188-1.3594 2.1562-2.625 2.1562-11.016 1.5469-22.125 2.4375-33.234 2.625-11.391-0.1875-22.734-1.0312-33.984-2.625-1.3125-0.14062-2.3438-1.2188-2.5312-2.5312l-14.297-49.078h0.046875c-5.0156-15.703-17.109-28.219-32.672-33.703-6.7031-2.3906-12.844-4.9219-19.453-7.9219h0.046875c-7.0781-3-14.672-4.5469-22.359-4.5469-8.6719-0.046875-17.25 2.0625-24.938 6.0938l-45.938 24.938v0.046875c-1.2188 0.79688-2.7656 0.79688-3.9844 0-24.328-17.906-46.125-38.906-64.922-62.531-0.65625-0.98438-0.65625-2.25 0-3.2344l25.781-44.531c8.2031-14.297 8.7656-31.734 1.5938-46.547-3.1406-6.4688-5.7656-12-8.0625-18.609-5.8594-15.609-18.844-27.469-34.922-31.922l-50.766-13.453c-1.0312-0.1875-1.875-0.89062-2.25-1.9219-4.2188-29.578-4.2188-59.578 0-89.156 0.51562-0.9375 1.4531-1.6406 2.5312-1.9219l50.484-13.547c15.984-4.3594 28.922-16.078 34.828-31.547 2.4375-6.375 5.2031-12.656 8.25-18.75 7.125-14.906 6.4688-32.297-1.6875-46.641l-25.781-44.156v-0.046875c-0.51562-0.89062-0.51562-2.0625 0-3 18.75-23.766 40.5-44.953 64.781-63 1.4062-0.70312 3.0938-0.70312 4.4531 0l45.938 25.078h0.046875c14.719 7.7344 32.156 8.1562 47.25 1.2188 7.3125-3.2344 13.781-5.5312 19.312-7.5469 15.609-5.4844 27.703-17.953 32.672-33.75l14.391-49.219v0.046875c0.32812-1.2188 1.3594-2.1094 2.625-2.1562 12-1.6875 22.547-2.625 33-3.1406 10.453-0.46875 22.453 1.4531 34.453 3.1406 1.1719 0.32812 2.1094 1.3125 2.2969 2.5312l14.297 49.078h-0.046875c5.1094 15.609 17.156 27.984 32.672 33.469 5.5312 2.0625 12 4.3125 19.219 7.5469h-0.046875c15.141 6.9375 32.625 6.5156 47.391-1.2188l45.938-25.078h0.046875c1.3594-0.70312 2.9531-0.70312 4.3125 0 24.328 18.094 46.125 39.328 64.781 63.141 0.60938 0.89062 0.60938 2.1094 0 3l-25.875 43.781c-8.2031 14.438-8.7656 31.969-1.5938 46.922 3.0469 6.0469 5.7656 12.188 8.1562 18.516 5.8594 15.516 18.797 27.375 34.828 31.781l50.766 13.781c1.0312 0.28125 1.9688 0.98438 2.4844 1.9219 4.125 29.625 4.0781 59.719-0.23438 89.297z"/>
<path d="m600 414.24c-49.266 0-96.516 19.594-131.34 54.422-34.828 34.828-54.422 82.078-54.422 131.34s19.594 96.516 54.422 131.34c34.828 34.828 82.078 54.422 131.34 54.422s96.516-19.594 131.34-54.422c34.828-34.828 54.422-82.078 54.422-131.34-0.046875-49.266-19.641-96.469-54.469-131.29s-82.031-54.422-131.29-54.469zm0 348c-65.812 0-125.11-39.656-150.24-100.45-25.172-60.797-11.203-130.78 35.391-177.24 46.594-46.5 116.58-60.328 177.32-35.016 60.75 25.266 100.27 84.656 100.12 150.47-0.14062 89.766-72.844 162.47-162.61 162.61z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

+46 -6
View File
@@ -1,5 +1,6 @@
---
import '../ui/crt.css'
import gearIcon from '../assets/noun-gear-8323296.svg?url'
---
<html lang="en">
@@ -9,13 +10,44 @@ import '../ui/crt.css'
<title>Halfstreet — Ethan J Lewis</title>
<meta name="description" content="A gothic mystery." />
<meta name="robots" content="noindex" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon-96x96.png" type="image/png" sizes="96x96" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#1a0d00" />
</head>
<body>
<div class="mystery-root" data-mystery-root>
<div class="mystery-bezel">
<div class="mystery-theme-toggle" data-mystery-theme-toggle>
<button type="button" data-theme-choice="amber" aria-pressed="true">[B]</button>
<button type="button" data-theme-choice="ansi" aria-pressed="false">[C]</button>
<div class="mystery-options" data-mystery-options>
<button
type="button"
class="mystery-options-toggle"
data-options-toggle
aria-label="Options"
aria-expanded="false"
style={`--gear-icon: url(${gearIcon})`}
>
<span class="mystery-options-icon" aria-hidden="true"></span>
</button>
<div class="mystery-options-menu" data-options-menu hidden>
<div class="mystery-options-group" aria-label="Screen">
<div class="mystery-options-label">Screen</div>
<button type="button" data-theme-choice="amber" aria-pressed="true">Amber</button>
<button type="button" data-theme-choice="ansi" aria-pressed="false">ANSI</button>
</div>
<div class="mystery-options-group" aria-label="Cursor">
<div class="mystery-options-label">Cursor</div>
<button type="button" data-cursor-choice="bar" aria-pressed="true">Bar</button>
<button type="button" data-cursor-choice="block" aria-pressed="false">Block</button>
<button type="button" data-cursor-choice="underscore" aria-pressed="false">Underline</button>
</div>
<div class="mystery-options-group" aria-label="Game">
<div class="mystery-options-label">Game</div>
<button type="button" data-restart-choice>Restart</button>
</div>
</div>
</div>
<div class="mystery-transcript" data-mystery-transcript aria-live="polite" aria-atomic="false"></div>
<div class="mystery-chips" data-mystery-chips></div>
@@ -30,14 +62,15 @@ import '../ui/crt.css'
spellcheck="false"
aria-label="Command input"
/>
<span class="mystery-input-display" data-mystery-input-display aria-hidden="true"></span>
</div>
</div>
<footer class="mystery-footer">
By <a href="https://ethanjlewis.com">Ethan J Lewis</a>
© 2026 <a href="https://ethanjlewis.com">Ethan J Lewis</a>
<span aria-hidden="true">|</span>
<a href="https://half.st/ejlewis/halfstreet/src/branch/main/LICENSE">GNU 3.0</a>
<span aria-hidden="true">|</span>
<a href="https://half.st/ejlewis/halfstreet">Source Code</a>
<span aria-hidden="true">|</span>
<a href="https://half.st/ejlewis/halfstreet/src/branch/main/LICENSE">GNU General Public License v3.0</a>
</footer>
</div>
<script>
@@ -46,7 +79,14 @@ import '../ui/crt.css'
const stored = (() => {
try { return localStorage.getItem('halfstreet:theme:v1') } catch { return null }
})()
const storedCursor = (() => {
try { return localStorage.getItem('halfstreet:cursor:v1') } catch { return null }
})()
document.documentElement.setAttribute('data-mystery-theme', stored === 'ansi' ? 'ansi' : 'amber')
document.documentElement.setAttribute(
'data-mystery-cursor',
storedCursor === 'block' || storedCursor === 'underscore' ? storedCursor : 'bar',
)
</script>
<script>
import '../ui/terminal.ts'
+145 -10
View File
@@ -26,6 +26,28 @@ body {
--m-divider-style: double;
}
:root[data-mystery-cursor='bar'],
:root:not([data-mystery-cursor]) {
--m-cursor-preview: '|';
--m-cursor-preview-size: 1em;
--m-cursor-preview-weight: normal;
--m-cursor-preview-offset: 0;
}
:root[data-mystery-cursor='block'] {
--m-cursor-preview: '█';
--m-cursor-preview-size: 0.9em;
--m-cursor-preview-weight: normal;
--m-cursor-preview-offset: 0;
}
:root[data-mystery-cursor='underscore'] {
--m-cursor-preview: '_';
--m-cursor-preview-size: 1.15em;
--m-cursor-preview-weight: bold;
--m-cursor-preview-offset: 0.18em;
}
.mystery-root {
position: fixed;
inset: 0;
@@ -93,17 +115,73 @@ body {
z-index: 1;
}
.mystery-theme-toggle {
.mystery-options {
position: absolute;
top: 8px;
right: 12px;
right: 32px;
z-index: 3;
display: flex;
gap: 6px;
font-size: 11px;
}
.mystery-theme-toggle button {
.mystery-options-toggle {
width: 34px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
position: relative;
padding: 0;
}
.mystery-options-icon {
width: 23px;
height: 23px;
background: currentColor;
display: block;
mask: var(--gear-icon) center / contain no-repeat;
-webkit-mask: var(--gear-icon) center / contain no-repeat;
}
.mystery-options-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 180px;
padding: 8px;
background: var(--m-bg);
border: 1px solid var(--m-dim);
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45);
}
.mystery-options-menu::after {
content: 'Cursor ' var(--m-cursor-preview);
display: block;
margin-top: 8px;
padding-top: 7px;
border-top: 1px solid var(--m-dim);
color: var(--m-fg);
font-size: 12px;
font-weight: var(--m-cursor-preview-weight);
line-height: 1;
}
.mystery-options-group {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.mystery-options-group + .mystery-options-group {
margin-top: 8px;
}
.mystery-options-label {
flex: 0 0 100%;
color: var(--m-dim);
}
.mystery-options button {
background: transparent;
color: var(--m-dim);
border: 1px solid var(--m-dim);
@@ -113,7 +191,8 @@ body {
cursor: pointer;
}
.mystery-theme-toggle button[aria-pressed='true'] {
.mystery-options button[aria-pressed='true'],
.mystery-options-toggle[aria-expanded='true'] {
color: var(--m-fg);
border-color: var(--m-fg);
}
@@ -123,6 +202,8 @@ body {
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-color: var(--m-dim) transparent;
scrollbar-width: thin;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
@@ -131,6 +212,18 @@ body {
padding-right: 6px;
}
.mystery-transcript::-webkit-scrollbar {
width: 6px;
}
.mystery-transcript::-webkit-scrollbar-track {
background: transparent;
}
.mystery-transcript::-webkit-scrollbar-thumb {
background: var(--m-dim);
}
.mystery-transcript .system {
color: var(--m-accent-1);
font-weight: bold;
@@ -172,22 +265,64 @@ body {
margin-top: 10px;
position: relative;
z-index: 2;
min-height: 1.45em;
cursor: text;
}
.mystery-input-row::before {
content: '>';
color: var(--m-accent-2);
flex: 0 0 auto;
}
.mystery-input {
flex: 1;
position: absolute;
inset: 0;
width: 100%;
min-width: 0;
opacity: 0;
color: transparent;
caret-color: transparent;
background: transparent;
color: var(--m-fg);
border: none;
outline: none;
font: inherit;
text-shadow: inherit;
caret-color: var(--m-fg);
}
.mystery-input-display {
flex: 1;
min-width: 0;
color: var(--m-fg);
white-space: pre-wrap;
overflow-wrap: anywhere;
pointer-events: none;
}
.mystery-input-display[data-placeholder='true'] {
color: var(--m-dim);
font-style: italic;
}
.mystery-input-display::after {
content: var(--m-cursor-preview);
display: inline-block;
color: var(--m-fg);
font-size: var(--m-cursor-preview-size);
font-style: normal;
font-weight: var(--m-cursor-preview-weight);
line-height: 1;
transform: translateY(var(--m-cursor-preview-offset));
margin-left: 0.1ch;
opacity: 0;
}
.mystery-input:focus + .mystery-input-display::after {
animation: mystery-cursor-blink 1.05s steps(1, end) infinite;
}
@keyframes mystery-cursor-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.mystery-chips {
+95 -14
View File
@@ -10,6 +10,7 @@ import { renderChips } from './chip-render'
const transcriptEl = document.querySelector<HTMLDivElement>('[data-mystery-transcript]')
const inputEl = document.querySelector<HTMLInputElement>('[data-mystery-input]')
const inputDisplayEl = document.querySelector<HTMLSpanElement>('[data-mystery-input-display]')
const HELP_TEXT = `You arrive at the address, but you do not remember what has happened. The road behind you is gone...
@@ -30,13 +31,17 @@ theme change the terminal colors
Most commands are verb first, then the thing: examine gate, take lamp, use key on door.`
if (!transcriptEl || !inputEl) {
if (!transcriptEl || !inputEl || !inputDisplayEl) {
console.error('[halfstreet] terminal mount points missing')
} else {
const restored = loadState()
let state: GameState = restored ?? initialStateFor(world)
let lastState: GameState | null = null // for one-step undo
let transientHelpEl: HTMLDivElement | null = null
let commandHistory: string[] = []
let historyIndex: number | null = null
let historyDraft = ''
let idleHintTimer: number | null = null
if (!restored) {
// Fresh state already includes the opening narration in its transcript.
@@ -48,7 +53,9 @@ if (!transcriptEl || !inputEl) {
function refreshChips(): void {
renderChips(computeChips(state, world), (command) => {
clearIdleHint()
inputEl!.value = command
syncCommandLine()
if (command.endsWith(' ')) {
inputEl!.focus()
inputEl!.setSelectionRange(command.length, command.length)
@@ -66,6 +73,12 @@ if (!transcriptEl || !inputEl) {
inputEl!.classList.toggle('ended', state.endedWith !== null)
}
const syncCommandLine = (): void => {
const visibleText = inputEl.value || inputEl.placeholder
inputDisplayEl.textContent = visibleText
inputDisplayEl.dataset['placeholder'] = inputEl.value ? 'false' : inputEl.placeholder ? 'true' : 'false'
}
const buildParserContext = (s: GameState): ParserContext => {
const room = world.rooms[s.location]
const visibleNouns: { id: string; aliases: string[] }[] = []
@@ -110,6 +123,23 @@ if (!transcriptEl || !inputEl) {
transcriptEl.scrollTop = transcriptEl.scrollHeight
}
const clearIdleHint = (): void => {
if (idleHintTimer !== null) {
window.clearTimeout(idleHintTimer)
idleHintTimer = null
}
inputEl.placeholder = ''
syncCommandLine()
}
const scheduleIdleHint = (): void => {
clearIdleHint()
idleHintTimer = window.setTimeout(() => {
inputEl.placeholder = 'type here...'
syncCommandLine()
}, 30000)
}
const clearTransientHelp = (): void => {
transientHelpEl?.remove()
transientHelpEl = null
@@ -136,18 +166,58 @@ if (!transcriptEl || !inputEl) {
renderAll(lines)
}
const restart = (): void => {
const confirmed = confirm('Restart? Your progress will be lost.')
if (!confirmed) {
appendLines([{ kind: 'system', text: '(restart cancelled)' }])
return
}
clearSave()
state = initialStateFor(world)
transcriptEl.innerHTML = ''
inputEl.value = ''
syncCommandLine()
renderAll(state.transcript)
saveState(state)
refreshChips()
syncEndedUI()
}
renderAll(state.transcript)
refreshChips()
syncEndedUI()
inputEl.focus()
syncCommandLine()
scheduleIdleHint()
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
if (commandHistory.length === 0) return
e.preventDefault()
if (historyIndex === null) {
historyDraft = inputEl.value
historyIndex = commandHistory.length
}
if (e.key === 'ArrowUp') {
historyIndex = Math.max(0, historyIndex - 1)
} else {
historyIndex = Math.min(commandHistory.length, historyIndex + 1)
}
inputEl.value = historyIndex === commandHistory.length ? historyDraft : commandHistory[historyIndex]!
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length)
syncCommandLine()
return
}
if (e.key !== 'Enter') return
e.preventDefault()
const raw = inputEl.value
inputEl.value = ''
syncCommandLine()
if (!raw.trim()) return
clearTransientHelp()
clearIdleHint()
commandHistory = [...commandHistory, raw].slice(-50)
historyIndex = null
historyDraft = ''
appendLines([{ kind: 'player', text: raw }])
// Once the game has ended, only restart and undo are allowed.
@@ -162,18 +232,7 @@ if (!transcriptEl || !inputEl) {
// Engine-level meta-commands handled here so the engine stays pure.
const trimmed = raw.trim().toLowerCase()
if (trimmed === 'restart') {
const confirmed = confirm('Restart? Your progress will be lost.')
if (!confirmed) {
appendLines([{ kind: 'system', text: '(restart cancelled)' }])
return
}
clearSave()
state = initialStateFor(world)
transcriptEl.innerHTML = ''
renderAll(state.transcript)
saveState(state)
refreshChips()
syncEndedUI()
restart()
return
}
if (trimmed === 'help') {
@@ -220,10 +279,32 @@ if (!transcriptEl || !inputEl) {
}
})
inputEl.addEventListener('input', syncCommandLine)
inputEl.addEventListener('focus', clearIdleHint)
inputEl.addEventListener('pointerdown', clearIdleHint)
inputEl.parentElement?.addEventListener('pointerdown', () => {
inputEl.focus()
})
document.addEventListener('keydown', (e) => {
const target = e.target as HTMLElement | null
const isEditable =
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.isContentEditable === true
if (e.key === '/' && !isEditable) {
e.preventDefault()
clearIdleHint()
inputEl.focus()
inputEl.setSelectionRange(inputEl.value.length, inputEl.value.length)
return
}
if (e.key === 'Escape') {
saveState(state)
window.location.href = '/'
}
})
document.addEventListener('halfstreet-restart', restart)
}
+61
View File
@@ -1,6 +1,8 @@
const STORAGE_KEY = 'halfstreet:theme:v1'
const CURSOR_STORAGE_KEY = 'halfstreet:cursor:v1'
type Theme = 'amber' | 'ansi'
type Cursor = 'bar' | 'block' | 'underscore'
function getStored(): Theme {
try {
@@ -10,6 +12,15 @@ function getStored(): Theme {
}
}
function getStoredCursor(): Cursor {
try {
const stored = localStorage.getItem(CURSOR_STORAGE_KEY)
return stored === 'block' || stored === 'underscore' ? stored : 'bar'
} catch {
return 'bar'
}
}
function setTheme(theme: Theme): void {
document.documentElement.setAttribute('data-mystery-theme', theme)
try {
@@ -22,8 +33,46 @@ function setTheme(theme: Theme): void {
}
}
function setCursor(cursor: Cursor): void {
document.documentElement.setAttribute('data-mystery-cursor', cursor)
try {
localStorage.setItem(CURSOR_STORAGE_KEY, cursor)
} catch {
// ignore
}
for (const btn of document.querySelectorAll<HTMLButtonElement>('[data-cursor-choice]')) {
btn.setAttribute('aria-pressed', btn.dataset['cursorChoice'] === cursor ? 'true' : 'false')
}
}
const initial = getStored()
setTheme(initial)
setCursor(getStoredCursor())
const optionsRoot = document.querySelector<HTMLElement>('[data-mystery-options]')
const optionsToggle = document.querySelector<HTMLButtonElement>('[data-options-toggle]')
const optionsMenu = document.querySelector<HTMLElement>('[data-options-menu]')
function setOptionsOpen(open: boolean): void {
if (!optionsToggle || !optionsMenu) return
optionsToggle.setAttribute('aria-expanded', open ? 'true' : 'false')
optionsMenu.hidden = !open
document.documentElement.toggleAttribute('data-mystery-options-open', open)
}
optionsToggle?.addEventListener('click', () => {
setOptionsOpen(optionsToggle.getAttribute('aria-expanded') !== 'true')
})
document.addEventListener('click', (event) => {
if (!optionsRoot || !optionsRoot.contains(event.target as Node)) {
setOptionsOpen(false)
}
})
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setOptionsOpen(false)
})
document.querySelectorAll<HTMLButtonElement>('[data-theme-choice]').forEach((btn) => {
btn.addEventListener('click', () => {
@@ -32,6 +81,18 @@ document.querySelectorAll<HTMLButtonElement>('[data-theme-choice]').forEach((btn
})
})
document.querySelectorAll<HTMLButtonElement>('[data-cursor-choice]').forEach((btn) => {
btn.addEventListener('click', () => {
const next = (btn.dataset['cursorChoice'] as Cursor | undefined) ?? 'bar'
setCursor(next)
})
})
document.querySelector<HTMLButtonElement>('[data-restart-choice]')?.addEventListener('click', () => {
setOptionsOpen(false)
document.dispatchEvent(new CustomEvent('halfstreet-restart'))
})
// 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', () => {
+13 -3
View File
@@ -13,6 +13,16 @@
- [x] Fix mobile - ascii text art at beginning too big to render
- [ ] Implement a simple "stealth mechanic", where sometimes it's advantageous to have the light out.
- [ ] Implement a simple (optional?) minimap in the UI? - Maybe tied to an item? Once you get the map the minimap appears? Can we POC it?
- [ ] Feature: Ability to retain console history, e.g., scroll through previous commands with up and down arrows.
- [ ] Feature: Grey italicized "type here..." text that appears near the terminal if the user doesn't click into the terminal within 30 seconds of entering the game or click the help button. The text disappears once a user clicks in the terminal, or selects a card.
- [ ]
- [x] Feature: Ability to retain console history, e.g., scroll through previous commands with up and down arrows.
- [x] Feature: Grey italicized "type here..." text that appears near the terminal if the user doesn't click into the terminal within 30 seconds of entering the game or click the help button. The text disappears once a user clicks in the terminal, or selects a card.
- [x] Change footer to "Copyright (C) 2026 [Ethan J Lewis](https://ethanjlewis.com) | [GNU 3.0](https://half.st/ejlewis/halfstreet/src/branch/main/LICENSE)| [Source Code](https://half.st/ejlewis/halfstreet)"
- [x] Wire up favicon
- [x] Feature: Provide ability to change cursor type. Block, underline, and whatever we have now.
- [x] Feature: Move cursor selection and screen toggle to option menu. Put the option menu where the screen toggle currently is. Give it a gear icon and let it be a tile.
- [x] Bug: gear icon too small.
- [x] Bug: option for cursor should show cursor in terminal when option menu is open.
- [x] Bug: cursor toggle not affecting the cursor at all.
- [x] Bug: idle text appearing above the tiles instead of in the terminal line.
- [x] Feature: / brings focus to terminal
- [x] Feature: Add "Restart" option to option menu
- [x] Bug: gear icon is still wayyyyyyy toooo smallllll it needs to be like 4x larger at least.