Design pass (all themes + chat UI), model picker, reduce-motion toggle

- 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.
This commit is contained in:
Martin Dvořák 2026-10-14 05:06:02 +00:00
commit 6635b5d405
29 changed files with 468 additions and 90 deletions

View file

@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next";
import { IceBackdrop } from "@/components/IceBackdrop";
import { MotionPreference } from "@/components/MotionPreference";
import { CookieConsent } from "@/components/CookieConsent";
import { InstallSheet } from "@/components/InstallSheet";
import { OfflineBanner } from "@/components/OfflineBanner";
@ -86,7 +87,7 @@ export const viewport: Viewport = {
* on the bright wallpaper is what made the first load look washed out until
* a theme toggle re-applied the classes.
*/
const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=a?false:(s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',!!d);if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`;
const themeScript = `(function(){try{var DEV=${devInstanceJs};var s=localStorage.getItem('freezmo.theme');var a=s==='aero';var d=a?false:(s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',!!d);if(a)document.documentElement.classList.add('aero');var l=localStorage.getItem('freezmo.locale.v1');if(l==='cs'||l==='en')document.documentElement.lang=l;if(localStorage.getItem('freezmo.reduce-motion.v1')==='1')document.documentElement.classList.add('reduce-motion');var m=document.querySelector('meta[name="theme-color"][data-app]');if(!m){m=document.createElement('meta');m.setAttribute('name','theme-color');m.setAttribute('data-app','1');document.head.insertBefore(m,document.head.firstChild);}m.setAttribute('content',DEV?'#b45309':(a?'#2fa7e6':(d?'#0e1424':'#f4f7fd')));}catch(e){}})();`;
/**
* Captures Chrome's `beforeinstallprompt` (it can fire before React hydrates)
@ -105,13 +106,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<script dangerouslySetInnerHTML={{ __html: installScript }} />
</head>
<body className="min-h-full antialiased app-shell-in">
<IceBackdrop />
{children}
<InstallSheet />
<OfflineBanner />
<CookieConsent />
<Toaster />
<ServiceWorkerRegistrar />
<MotionPreference>
<IceBackdrop />
{children}
<InstallSheet />
<OfflineBanner />
<CookieConsent />
<Toaster />
<ServiceWorkerRegistrar />
</MotionPreference>
</body>
</html>
);