aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-04-22 09:19:50 +0530
committerBobby <[email protected]>2026-04-22 09:19:50 +0530
commit7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8 (patch)
tree85772db514216c5d71f7963b7a730b164f09bff6
parent3184ffcec523d19dafa11955778a2e5dafc23843 (diff)
downloadhollowdark-7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8.tar.xz
hollowdark-7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8.zip
Rebuild Settings with Reading, Sound, Accessibility, and About sections per the mockup
-rw-r--r--lib/accessibility/state.ts14
-rw-r--r--lib/components/TextSizeChoice.svelte89
-rw-r--r--lib/reading/state.ts11
-rw-r--r--lib/screens/SettingsScreen.svelte106
-rw-r--r--routes/+layout.svelte25
-rw-r--r--routes/settings/+page.svelte6
-rw-r--r--static/css/app.css42
7 files changed, 281 insertions, 12 deletions
diff --git a/lib/accessibility/state.ts b/lib/accessibility/state.ts
new file mode 100644
index 0000000..4736ef5
--- /dev/null
+++ b/lib/accessibility/state.ts
@@ -0,0 +1,14 @@
+import { writable, type Writable } from 'svelte/store'
+
+/**
+ * User override that strengthens the `prefers-reduced-motion` media
+ * query. When true, long transitions are trimmed regardless of the OS
+ * setting. Applied as a class on `<html>`.
+ */
+export const reduceMotion: Writable<boolean> = writable(false)
+
+/**
+ * User override that brightens the foreground palette for readers who
+ * need extra contrast. Applied as a class on `<html>`.
+ */
+export const highContrast: Writable<boolean> = writable(false)
diff --git a/lib/components/TextSizeChoice.svelte b/lib/components/TextSizeChoice.svelte
new file mode 100644
index 0000000..f508c84
--- /dev/null
+++ b/lib/components/TextSizeChoice.svelte
@@ -0,0 +1,89 @@
+<script lang="ts">
+ import type { TextSize } from '@hollowdark/lib/reading/state'
+
+ interface Option {
+ value: TextSize
+ label: string
+ }
+
+ interface Props {
+ value: TextSize
+ onChange: (next: TextSize) => void
+ }
+
+ let { value, onChange }: Props = $props()
+
+ const options: readonly Option[] = [
+ { value: 'small', label: 'Small' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'large', label: 'Large' },
+ { value: 'extra-large', label: 'XL' }
+ ]
+</script>
+
+<div class="group" role="radiogroup" aria-label="Text size">
+ {#each options as option (option.value)}
+ <button
+ type="button"
+ role="radio"
+ aria-checked={value === option.value}
+ class="choice"
+ class:active={value === option.value}
+ class:size-small={option.value === 'small'}
+ class:size-medium={option.value === 'medium'}
+ class:size-large={option.value === 'large'}
+ class:size-extra-large={option.value === 'extra-large'}
+ onclick={() => onChange(option.value)}
+ >
+ {option.label}
+ </button>
+ {/each}
+</div>
+
+<style>
+ .group {
+ display: flex;
+ gap: var(--space-2);
+ }
+
+ .choice {
+ flex: 1;
+ background: transparent;
+ border: 1px solid rgba(232, 226, 213, 0.12);
+ border-radius: 6px;
+ padding: var(--space-2) var(--space-3);
+ font-family: var(--font-body);
+ color: var(--color-text-secondary);
+ transition:
+ color var(--transition-fast),
+ border-color var(--transition-fast),
+ background var(--transition-fast);
+ }
+
+ .choice:hover {
+ color: var(--color-text);
+ border-color: rgba(232, 226, 213, 0.22);
+ }
+
+ .choice.active {
+ color: var(--color-accent);
+ border-color: rgba(184, 136, 74, 0.4);
+ background: rgba(184, 136, 74, 0.12);
+ }
+
+ .choice.size-small {
+ font-size: 13px;
+ }
+
+ .choice.size-medium {
+ font-size: 15px;
+ }
+
+ .choice.size-large {
+ font-size: 17px;
+ }
+
+ .choice.size-extra-large {
+ font-size: 19px;
+ }
+</style>
diff --git a/lib/reading/state.ts b/lib/reading/state.ts
new file mode 100644
index 0000000..5194d0a
--- /dev/null
+++ b/lib/reading/state.ts
@@ -0,0 +1,11 @@
+import { writable, type Writable } from 'svelte/store'
+
+/** Text-size scale presets. Each maps to a class on the document root. */
+export type TextSize = 'small' | 'medium' | 'large' | 'extra-large'
+
+/**
+ * Reader's chosen text size. Drives the `--text-*` custom properties
+ * through a class on `<html>` that swaps the whole scale at once. The
+ * layout subscribes to this store and applies the class.
+ */
+export const textSize: Writable<TextSize> = writable('medium')
diff --git a/lib/screens/SettingsScreen.svelte b/lib/screens/SettingsScreen.svelte
index 434b9c9..63b6ba0 100644
--- a/lib/screens/SettingsScreen.svelte
+++ b/lib/screens/SettingsScreen.svelte
@@ -1,18 +1,30 @@
<script lang="ts">
import Slider from '@hollowdark/lib/components/Slider.svelte'
+ import TextSizeChoice from '@hollowdark/lib/components/TextSizeChoice.svelte'
import ToggleSwitch from '@hollowdark/lib/components/ToggleSwitch.svelte'
import { ambientVolume, masterMuted } from '@hollowdark/lib/audio/state'
- import { reduceMotion } from '@hollowdark/lib/display/state'
+ import { highContrast, reduceMotion } from '@hollowdark/lib/accessibility/state'
+ import { textSize, type TextSize } from '@hollowdark/lib/reading/state'
import { APP_VERSION_FULL } from '@hollowdark/lib/version/version'
interface Props {
onBack: () => void
+ onCredits: () => void
}
- let { onBack }: Props = $props()
+ let { onBack, onCredits }: Props = $props()
const volumePercent = $derived(Math.round($ambientVolume * 100))
+ const TEXT_SIZE_LABELS: Record<TextSize, string> = {
+ small: 'Small',
+ medium: 'Medium',
+ large: 'Large',
+ 'extra-large': 'Extra large'
+ }
+
+ const currentTextSizeLabel = $derived(TEXT_SIZE_LABELS[$textSize])
+
function setMuted(next: boolean): void {
masterMuted.set(next)
}
@@ -21,9 +33,17 @@
ambientVolume.set(next)
}
+ function setTextSize(next: TextSize): void {
+ textSize.set(next)
+ }
+
function setReduceMotion(next: boolean): void {
reduceMotion.set(next)
}
+
+ function setHighContrast(next: boolean): void {
+ highContrast.set(next)
+ }
</script>
<section class="settings">
@@ -34,14 +54,33 @@
<div class="body">
<section class="group">
- <h2 class="group-label">Audio</h2>
+ <h2 class="group-label">Reading</h2>
+
+ <div class="row stacked">
+ <div class="row-label">
+ <p class="row-name">Text size</p>
+ <p class="row-hint">Affects headings, menus, and the reading column.</p>
+ </div>
+ <p class="row-value">{currentTextSizeLabel}</p>
+ </div>
+ <div class="row-body">
+ <TextSizeChoice value={$textSize} onChange={setTextSize} />
+ </div>
+ </section>
+
+ <section class="group">
+ <h2 class="group-label">Sound</h2>
<div class="row">
<div class="row-label">
<p class="row-name">Mute everything</p>
<p class="row-hint">Silence all sound, regardless of volume.</p>
</div>
- <ToggleSwitch value={$masterMuted} label="Mute everything" onChange={setMuted} />
+ <ToggleSwitch
+ value={$masterMuted}
+ label="Mute everything"
+ onChange={setMuted}
+ />
</div>
<div class="row">
@@ -64,15 +103,12 @@
</section>
<section class="group">
- <h2 class="group-label">Display</h2>
+ <h2 class="group-label">Accessibility</h2>
<div class="row">
<div class="row-label">
<p class="row-name">Reduce motion</p>
- <p class="row-hint">
- Hide the falling leaves and trim long transitions. Overrides
- the system preference.
- </p>
+ <p class="row-hint">Trim transitions and suppress animated detail.</p>
</div>
<ToggleSwitch
value={$reduceMotion}
@@ -80,6 +116,18 @@
onChange={setReduceMotion}
/>
</div>
+
+ <div class="row">
+ <div class="row-label">
+ <p class="row-name">Higher contrast</p>
+ <p class="row-hint">Brighten text against the background.</p>
+ </div>
+ <ToggleSwitch
+ value={$highContrast}
+ label="Higher contrast"
+ onChange={setHighContrast}
+ />
+ </div>
</section>
<section class="group">
@@ -87,8 +135,9 @@
<div class="about">
<p class="about-title">Hollowdark</p>
- <p class="about-line">Version {APP_VERSION_FULL}</p>
<p class="about-line">A literary life simulation.</p>
+ <p class="about-line muted">Version {APP_VERSION_FULL}</p>
+ <button class="credits-link" onclick={onCredits}>Credits ›</button>
</div>
</section>
</div>
@@ -163,6 +212,16 @@
border-bottom: 1px solid rgba(232, 226, 213, 0.06);
}
+ .row.stacked {
+ border-bottom: none;
+ padding-bottom: 0;
+ }
+
+ .row-body {
+ padding: 0 0 var(--space-3);
+ border-bottom: 1px solid rgba(232, 226, 213, 0.06);
+ }
+
.row-label {
display: flex;
flex-direction: column;
@@ -185,6 +244,13 @@
margin: 0;
}
+ .row-value {
+ font-family: var(--font-ui);
+ font-size: var(--text-sm);
+ color: var(--color-text-secondary);
+ margin: 0;
+ }
+
.slider-cell {
display: flex;
align-items: center;
@@ -217,7 +283,25 @@
.about-line {
font-family: var(--font-ui);
font-size: var(--text-sm);
- color: var(--color-text-tertiary);
+ color: var(--color-text-secondary);
margin: 0;
}
+
+ .about-line.muted {
+ color: var(--color-text-tertiary);
+ }
+
+ .credits-link {
+ align-self: flex-start;
+ margin-top: var(--space-3);
+ font-family: var(--font-ui);
+ font-size: var(--text-sm);
+ color: var(--color-accent);
+ letter-spacing: 0.3px;
+ transition: color var(--transition-fast);
+ }
+
+ .credits-link:hover {
+ color: var(--color-text);
+ }
</style>
diff --git a/routes/+layout.svelte b/routes/+layout.svelte
index 226487d..695e64d 100644
--- a/routes/+layout.svelte
+++ b/routes/+layout.svelte
@@ -1,10 +1,35 @@
<script lang="ts">
import { base } from '$app/paths'
import AudioPlayer from '@hollowdark/lib/components/AudioPlayer.svelte'
+ import { highContrast, reduceMotion } from '@hollowdark/lib/accessibility/state'
+ import { textSize, type TextSize } from '@hollowdark/lib/reading/state'
let { children } = $props()
const titleTrackSrc = `${base}/audio/title/piano-relaxing.mp3`
+
+ const ALL_TEXT_SIZE_CLASSES: readonly string[] = [
+ 'text-size-small',
+ 'text-size-medium',
+ 'text-size-large',
+ 'text-size-extra-large'
+ ]
+
+ function applyTextSize(size: TextSize): void {
+ if (typeof document === 'undefined') return
+ const root = document.documentElement
+ root.classList.remove(...ALL_TEXT_SIZE_CLASSES)
+ root.classList.add(`text-size-${size}`)
+ }
+
+ function applyFlag(className: string, active: boolean): void {
+ if (typeof document === 'undefined') return
+ document.documentElement.classList.toggle(className, active)
+ }
+
+ $effect(() => applyTextSize($textSize))
+ $effect(() => applyFlag('reduce-motion', $reduceMotion))
+ $effect(() => applyFlag('high-contrast', $highContrast))
</script>
<AudioPlayer src={titleTrackSrc} loop />
diff --git a/routes/settings/+page.svelte b/routes/settings/+page.svelte
index 4e5a080..b8c6e1f 100644
--- a/routes/settings/+page.svelte
+++ b/routes/settings/+page.svelte
@@ -6,6 +6,10 @@
function handleBack(): void {
goto(resolve('/'))
}
+
+ function handleCredits(): void {
+ goto(resolve('/credits'))
+ }
</script>
-<SettingsScreen onBack={handleBack} />
+<SettingsScreen onBack={handleBack} onCredits={handleCredits} />
diff --git a/static/css/app.css b/static/css/app.css
index cc210a3..8d33846 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -40,6 +40,48 @@ body.crisis-mode {
--color-accent: var(--color-accent-warm);
}
+html.text-size-small {
+ --text-xs: 10px;
+ --text-sm: 12px;
+ --text-base: 14px;
+ --text-md: 16px;
+ --text-lg: 19px;
+ --text-xl: 24px;
+}
+
+html.text-size-large {
+ --text-xs: 12px;
+ --text-sm: 14px;
+ --text-base: 18px;
+ --text-md: 20px;
+ --text-lg: 24px;
+ --text-xl: 32px;
+}
+
+html.text-size-extra-large {
+ --text-xs: 13px;
+ --text-sm: 15px;
+ --text-base: 20px;
+ --text-md: 22px;
+ --text-lg: 27px;
+ --text-xl: 36px;
+}
+
+html.high-contrast {
+ --color-text: #f3ecdc;
+ --color-text-secondary: #b4ac9c;
+ --color-text-tertiary: #857f72;
+}
+
+html.reduce-motion *,
+html.reduce-motion *::before,
+html.reduce-motion *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+}
+
*,
*::before,
*::after {