diff options
| author | Bobby <[email protected]> | 2026-04-22 09:19:50 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-04-22 09:19:50 +0530 |
| commit | 7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8 (patch) | |
| tree | 85772db514216c5d71f7963b7a730b164f09bff6 | |
| parent | 3184ffcec523d19dafa11955778a2e5dafc23843 (diff) | |
| download | hollowdark-7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8.tar.xz hollowdark-7d21ed8f09e60d3ed962bedcf304a5e17e07b2d8.zip | |
Rebuild Settings with Reading, Sound, Accessibility, and About sections per the mockup
| -rw-r--r-- | lib/accessibility/state.ts | 14 | ||||
| -rw-r--r-- | lib/components/TextSizeChoice.svelte | 89 | ||||
| -rw-r--r-- | lib/reading/state.ts | 11 | ||||
| -rw-r--r-- | lib/screens/SettingsScreen.svelte | 106 | ||||
| -rw-r--r-- | routes/+layout.svelte | 25 | ||||
| -rw-r--r-- | routes/settings/+page.svelte | 6 | ||||
| -rw-r--r-- | static/css/app.css | 42 |
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 { |
