- Aero: blue gradients back on the active segment and every primary
(Review due, the card's Study button, the composer's send button); a
frosted backing behind the page heading and the All decks label; a
bottom-weighted rail tint; chat header/composer/welcome panels; calmer
orbs; stronger card veil + a stronger blur bake.
- Dark/Light: a dedicated --track token makes progress bars read (the
first attempt shifted --surface-sunken and the pixel scan caught the
sheet-contrast regression); brighter dark secondary text; darker light
segmented labels and difficulty tints; stronger light card border and
shadow.
- Chat UI: friendly model chip ("Gemma 4 31B") with the raw name in a
tooltip, larger chips, plain centered disclaimer, vertically centered
welcome panel, distinct chat-plus icon for new chat.
- Chat models: every chat-capable model from the server's /api/tags is
selectable (favourites first, search field when long, live-list
validation in the route); default CHAT_MODEL_DEFAULT or gemma4:31b.
- Reduce motion: Settings toggle (default off) that parks the scene,
collapses CSS animations and drives framer-motion's calm mode.
- Folders hint, "New" badge on fresh decks.
Contrast: 0 findings, axe clean, mobile 126/126, chat 102/102, wizard
95/95, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.6, SW -> freezmo-v71.
51 lines
1.2 KiB
TypeScript
51 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useSyncExternalStore } from "react";
|
|
|
|
/**
|
|
* The Settings "reduce motion" toggle (default off). It mirrors the OS
|
|
* preference: the scene stops moving, CSS transitions collapse, and
|
|
* framer-motion runs in its calm mode via MotionPreference. One shared store,
|
|
* so the toggle and every consumer update together.
|
|
*/
|
|
|
|
const KEY = "freezmo.reduce-motion.v1";
|
|
|
|
let cached: boolean | null = null;
|
|
const listeners = new Set<() => void>();
|
|
|
|
function read(): boolean {
|
|
try {
|
|
return window.localStorage.getItem(KEY) === "1";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function snapshot(): boolean {
|
|
cached ??= read();
|
|
return cached;
|
|
}
|
|
|
|
export function applyReducedMotion(next: boolean) {
|
|
cached = next;
|
|
document.documentElement.classList.toggle("reduce-motion", next);
|
|
try {
|
|
window.localStorage.setItem(KEY, next ? "1" : "0");
|
|
} catch {
|
|
/* private mode */
|
|
}
|
|
listeners.forEach((listener) => listener());
|
|
}
|
|
|
|
export function useReducedMotionPref() {
|
|
const reduced = useSyncExternalStore(
|
|
(listener) => {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
},
|
|
snapshot,
|
|
() => false,
|
|
);
|
|
return { reduced, setReduced: applyReducedMotion };
|
|
}
|