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>
This commit is contained in:
@@ -232,17 +232,19 @@ Architectural finding: the v0.2 `SettingsPage.svelte` (2 891 LOC after Phase 4a)
|
|||||||
|
|
||||||
- Tab IDs in this commit map 1:1 to existing v0.2 section names (start-here / transcription / models / tasks / accessibility / privacy / advanced / help). Phase 4b will restructure to match the plan's canonical names (carving Output from Advanced and Vocabulary from Transcription).
|
- Tab IDs in this commit map 1:1 to existing v0.2 section names (start-here / transcription / models / tasks / accessibility / privacy / advanced / help). Phase 4b will restructure to match the plan's canonical names (carving Output from Advanced and Vocabulary from Transcription).
|
||||||
|
|
||||||
### Phase 4b — Settings restructure to canonical tab names. Pending.
|
### Phase 4b — Settings restructure to canonical tab names. Deferred to post-Phase-5.
|
||||||
|
|
||||||
- Carve **Output** out of `7. Advanced > Output & Capture` (currently at line 2398 in the pre-Phase-4a file) into its own top-level tab.
|
Rationale: phase 4a already delivers "one section at a time" under quietware. Renaming the eight tabs to match the canonical plan (Output / Vocabulary carved out as top-level) is labelling polish, not user-value. Page-skeleton sweep (Phase 5) delivers more visible improvement per LOC. Phase 4b will return after Phase 5 lands.
|
||||||
- Carve **Vocabulary** out of `2. Transcription > Custom vocabulary` (line 1472 in the pre-Phase-4a file) into its own top-level tab.
|
|
||||||
- Rename the remainder of `2. Transcription` to be subsumed by **Output** + **Vocabulary**, or kept as legacy section pending content audit.
|
|
||||||
- Final tab order: Start Here / Models / Output / Vocabulary / Tasks / Accessibility / Privacy / Advanced (eight tabs, matching the plan).
|
|
||||||
|
|
||||||
### Phase 4c — Texture-opacity slider + high-contrast toggle. Pending.
|
### Phase 4c — Texture-opacity slider + high-contrast toggle. Landed 2026-05-15.
|
||||||
|
|
||||||
- Texture-opacity slider in the Accessibility tab, range 0.05–0.08, default 0.06. Persists to existing preferences store.
|
- New static grain overlay defined in `src/design-system/v0.3-quietware-tokens.css`. Fixed-position `body::after`, `mix-blend-mode: overlay`, opacity driven by `--quietware-texture-opacity`. Pattern generated inline via SVG `feTurbulence` (`baseFrequency=0.85`, `numOctaves=2`, `stitchTiles=stitch`). Tile size 240×240, repeated across viewport. `pointer-events: none` so the overlay never intercepts clicks. Active only under `html[data-design="quietware"]` and only on the dark and light modes — high-contrast forces opacity to 0.
|
||||||
- High-contrast toggle in the Accessibility tab. Toggle overrides system `prefers-contrast: more` query when set.
|
- New `QuietwareAccessibilityControls.svelte` ([src/lib/components/](../../src/lib/components/QuietwareAccessibilityControls.svelte)) ships two controls:
|
||||||
|
- **High-contrast toggle**, wired through `LumotiaToggle`. Sets `<html data-contrast="high">` so the quietware HC palette block in the tokens CSS activates. Persists via `localStorage`. Overrides the OS `prefers-contrast: more` query when explicitly enabled.
|
||||||
|
- **Texture-intensity slider**, range 0.05–0.08, step 0.005, default 0.06. Sets the `--quietware-texture-opacity` CSS variable inline on `<html>` so it overrides the per-mode default in the tokens CSS. Disabled when high-contrast mode is on. Reset button restores the 0.06 default.
|
||||||
|
- Component injected into `SettingsPage.svelte` inside the Accessibility section, gated by `{#if isQuietware}`. v0.2 fallback unaffected.
|
||||||
|
- Persistence path: localStorage keys `lumotia:quietware:high-contrast` and `lumotia:quietware:texture-opacity`. Full integration with the central `Preferences` store is a follow-up; localStorage keeps Phase 4c contained.
|
||||||
|
- Verified: `npm run check` reports 0 errors, 0 warnings across 5706 files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,32 @@
|
|||||||
--quietware-texture-color: rgba(0, 0, 0, 0);
|
--quietware-texture-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Static grain overlay. Atmospheric texture across the whole viewport.
|
||||||
|
Fixed position so it does not scroll. Tile pattern generated inline
|
||||||
|
via SVG feTurbulence. Opacity driven by --quietware-texture-opacity
|
||||||
|
(default 0.06 dark / 0.07 light, slider range 0.05 to 0.08 in
|
||||||
|
advanced settings). mix-blend-mode overlay darkens shadows and
|
||||||
|
brightens highlights, producing the soft notebook-paper effect.
|
||||||
|
|
||||||
|
pointer-events: none so clicks pass through. z-index 9999 places it
|
||||||
|
above all content, but content remains fully interactive thanks to
|
||||||
|
the pointer-events override.
|
||||||
|
|
||||||
|
No texture in high-contrast mode — the [data-contrast="high"] block
|
||||||
|
above sets --quietware-texture-opacity to 0.
|
||||||
|
============================================================ */
|
||||||
|
:root[data-design="quietware"] body::after {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||||||
|
opacity: var(--quietware-texture-opacity);
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Reduced motion. Honour OS preference; quietware rules require it.
|
Reduced motion. Honour OS preference; quietware rules require it.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
140
src/lib/components/QuietwareAccessibilityControls.svelte
Normal file
140
src/lib/components/QuietwareAccessibilityControls.svelte
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<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>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import StatusPill from "$lib/ui/LumotiaStatusPill.svelte";
|
import StatusPill from "$lib/ui/LumotiaStatusPill.svelte";
|
||||||
import ZonePicker from "$lib/components/ZonePicker.svelte";
|
import ZonePicker from "$lib/components/ZonePicker.svelte";
|
||||||
import AccessibilityControls from "$lib/components/AccessibilityControls.svelte";
|
import AccessibilityControls from "$lib/components/AccessibilityControls.svelte";
|
||||||
|
import QuietwareAccessibilityControls from "$lib/components/QuietwareAccessibilityControls.svelte";
|
||||||
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
|
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
|
||||||
import { profilesStore, DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts";
|
import { profilesStore, DEFAULT_PROFILE_ID } from "$lib/stores/profiles.svelte.ts";
|
||||||
import { toasts } from "$lib/stores/toasts.svelte.js";
|
import { toasts } from "$lib/stores/toasts.svelte.js";
|
||||||
@@ -2164,10 +2165,13 @@
|
|||||||
<SettingsGroup
|
<SettingsGroup
|
||||||
title="Accessibility"
|
title="Accessibility"
|
||||||
description="Reduced motion, contrast, typography, and screen-reader hints."
|
description="Reduced motion, contrast, typography, and screen-reader hints."
|
||||||
open={searchActive ? searchMatches('Accessibility Reduced motion contrast typography lexend opendyslexic atkinson bionic dyslexic letter spacing line height') : false}
|
open={searchActive ? searchMatches('Accessibility Reduced motion contrast typography lexend opendyslexic atkinson bionic dyslexic letter spacing line height texture high contrast') : false}
|
||||||
>
|
>
|
||||||
<div class="animate-fade-in">
|
<div class="animate-fade-in">
|
||||||
<AccessibilityControls />
|
<AccessibilityControls />
|
||||||
|
{#if isQuietware}
|
||||||
|
<QuietwareAccessibilityControls />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user