Three additions land together so the slider is meaningful from day
one (instead of recording a preference that nothing consumes yet).
Static grain overlay.
- src/design-system/v0.3-quietware-tokens.css adds a fixed-position
body::after element that ships an SVG feTurbulence noise tile
repeated across the viewport. Opacity reads from
--quietware-texture-opacity. mix-blend-mode: overlay produces the
soft notebook-paper feel without obscuring content.
- pointer-events: none so clicks pass through. Active only under
html[data-design="quietware"]. High-contrast forces opacity to 0
via the existing HC palette block.
QuietwareAccessibilityControls component.
- NEW: src/lib/components/QuietwareAccessibilityControls.svelte.
Two controls, both quietware-only:
1. High-contrast toggle wires html[data-contrast="high"].
Overrides the OS prefers-contrast: more query when explicitly
on. Built on LumotiaToggle so it inherits the accessible
label + description pattern.
2. Texture-intensity slider, range 0.05 to 0.08, step 0.005,
default 0.06. Sets --quietware-texture-opacity inline on
<html>, overriding the per-mode default in the tokens CSS.
Disabled while high-contrast is on. Reset button restores
0.06.
- Persistence: localStorage keys
lumotia:quietware:high-contrast
lumotia:quietware:texture-opacity
so choices survive dev rebuilds and full restarts. Full
integration with the central Preferences store is a follow-up;
localStorage keeps Phase 4c contained.
SettingsPage Accessibility section.
- Imports the new QuietwareAccessibilityControls component.
- Renders {#if isQuietware} <QuietwareAccessibilityControls />
{/if} immediately after the existing AccessibilityControls block.
- Search filter copy extended to cover the new control vocabulary.
Verified.
- npm run check: 0 errors, 0 warnings across 5706 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
4.8 KiB
Svelte
141 lines
4.8 KiB
Svelte
<script lang="ts">
|
||
// v0.3 Phase 4c — quietware-specific accessibility controls.
|
||
//
|
||
// Renders only when html[data-design="quietware"] is set. Provides two
|
||
// controls that are not meaningful in the v0.2 surface:
|
||
//
|
||
// 1. High-contrast toggle. Sets html[data-contrast="high"] so the
|
||
// quietware HC palette block activates. Overrides the OS
|
||
// prefers-contrast: more query when explicitly enabled.
|
||
//
|
||
// 2. Texture-opacity slider. Range 0.05–0.08, default 0.06.
|
||
// Persists to localStorage and applies as an inline style on
|
||
// <html> so it overrides the default --quietware-texture-opacity
|
||
// set by the tokens CSS. The static grain overlay defined in
|
||
// v0.3-quietware-tokens.css consumes the variable.
|
||
//
|
||
// Both preferences persist via localStorage so the choice survives
|
||
// dev rebuilds and full restarts. Full integration with the central
|
||
// Preferences store is reserved for a follow-up commit; localStorage
|
||
// keeps Phase 4c contained.
|
||
import { onMount, onDestroy } from "svelte";
|
||
import LumotiaToggle from "$lib/ui/LumotiaToggle.svelte";
|
||
|
||
const HC_KEY = "lumotia:quietware:high-contrast";
|
||
const TEXTURE_KEY = "lumotia:quietware:texture-opacity";
|
||
|
||
const TEXTURE_MIN = 0.05;
|
||
const TEXTURE_MAX = 0.08;
|
||
const TEXTURE_STEP = 0.005;
|
||
const TEXTURE_DEFAULT = 0.06;
|
||
|
||
let highContrast = $state(false);
|
||
let textureOpacity = $state(TEXTURE_DEFAULT);
|
||
let mounted = $state(false);
|
||
|
||
function applyHighContrast(v: boolean) {
|
||
if (typeof document === "undefined") return;
|
||
if (v) document.documentElement.setAttribute("data-contrast", "high");
|
||
else document.documentElement.removeAttribute("data-contrast");
|
||
}
|
||
|
||
function applyTexture(v: number) {
|
||
if (typeof document === "undefined") return;
|
||
document.documentElement.style.setProperty("--quietware-texture-opacity", String(v));
|
||
}
|
||
|
||
onMount(() => {
|
||
if (typeof localStorage === "undefined") {
|
||
mounted = true;
|
||
return;
|
||
}
|
||
const hcRaw = localStorage.getItem(HC_KEY);
|
||
if (hcRaw === "1") highContrast = true;
|
||
const txRaw = localStorage.getItem(TEXTURE_KEY);
|
||
if (txRaw) {
|
||
const parsed = Number(txRaw);
|
||
if (Number.isFinite(parsed) && parsed >= TEXTURE_MIN && parsed <= TEXTURE_MAX) {
|
||
textureOpacity = parsed;
|
||
}
|
||
}
|
||
applyHighContrast(highContrast);
|
||
applyTexture(textureOpacity);
|
||
mounted = true;
|
||
});
|
||
|
||
onDestroy(() => {
|
||
// Leave the document attributes in place. Other surfaces may rely on
|
||
// them and we want HC + texture choices to persist beyond the
|
||
// settings panel's lifecycle.
|
||
});
|
||
|
||
$effect(() => {
|
||
if (!mounted) return;
|
||
applyHighContrast(highContrast);
|
||
try {
|
||
localStorage.setItem(HC_KEY, highContrast ? "1" : "0");
|
||
} catch {}
|
||
});
|
||
|
||
$effect(() => {
|
||
if (!mounted) return;
|
||
applyTexture(textureOpacity);
|
||
try {
|
||
localStorage.setItem(TEXTURE_KEY, String(textureOpacity));
|
||
} catch {}
|
||
});
|
||
|
||
function onTextureInput(event: Event) {
|
||
const v = Number((event.currentTarget as HTMLInputElement).value);
|
||
if (Number.isFinite(v)) textureOpacity = v;
|
||
}
|
||
|
||
function resetTexture() {
|
||
textureOpacity = TEXTURE_DEFAULT;
|
||
}
|
||
</script>
|
||
|
||
<div class="flex flex-col gap-5 pt-2 border-t border-border-subtle mt-4">
|
||
<p class="text-[12px] text-text-secondary font-medium">Tactile Quietware controls</p>
|
||
|
||
<!-- High-contrast toggle. LumotiaToggle wraps the label and
|
||
description so the control is programmatically associated. -->
|
||
<LumotiaToggle
|
||
bind:checked={highContrast}
|
||
label="High contrast"
|
||
description="Strip atmospheric texture and shift to a clarity-first palette. Overrides the OS prefers-contrast query when enabled."
|
||
/>
|
||
|
||
<!-- Texture-opacity slider -->
|
||
<div class="flex flex-col gap-2">
|
||
<div class="flex items-baseline justify-between">
|
||
<p class="text-[13px] font-medium text-text">Texture intensity</p>
|
||
<span class="text-[12px] text-text-tertiary font-mono">{(textureOpacity * 100).toFixed(1)}%</span>
|
||
</div>
|
||
<p class="text-[12px] text-text-secondary">
|
||
Strength of the static grain overlay. 5% baseline, 8% upper end.
|
||
Slider is disabled when high-contrast mode is active.
|
||
</p>
|
||
<div class="flex items-center gap-3">
|
||
<input
|
||
type="range"
|
||
min={TEXTURE_MIN}
|
||
max={TEXTURE_MAX}
|
||
step={TEXTURE_STEP}
|
||
value={textureOpacity}
|
||
oninput={onTextureInput}
|
||
disabled={highContrast}
|
||
aria-label="Texture intensity"
|
||
class="flex-1 accent-info disabled:opacity-50"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onclick={resetTexture}
|
||
disabled={highContrast}
|
||
class="px-2.5 py-1 text-[11px] rounded border border-border-subtle text-text-secondary
|
||
hover:text-text hover:border-border disabled:opacity-50"
|
||
>Reset</button>
|
||
</div>
|
||
</div>
|
||
</div>
|