Files
Lumotia/src/lib/components/QuietwareAccessibilityControls.svelte
Jake 4dca802f0b v0.3 Phase 4c: HC toggle + texture-intensity slider + grain overlay
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>
2026-05-15 13:43:58 +01:00

141 lines
4.8 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.050.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>