3 Commits

Author SHA1 Message Date
AnRil
9b488164e0 release(v0.3.3): акцентная типография — все надписи крупнее и контрастнее
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled
Release / Build installer + publish release (push) Has been cancelled
Жалоба: вторичные подписи (Активных / До следующего / Трекинг матчей /
Возобнови чтобы продолжить отсчёт) выглядели мелко и плохо читались.

Сделан sweep по всему UI:
- Базовая шкала secondary text: 12px → 13-14px
- Контрастность подписей: text-text/45 → text-text/65 (или /75 для лейблов)
- font-medium → font-semibold для метаданных карточек

Dashboard:
- Дата: 13/font-medium → 14/font-semibold
- HeroStat label: 12/medium/55 → 14/semibold/75 (вот эти "Активных" и пр.)
- HeroStat value: 26/semibold → 28/bold
- HeroStat subvalue: 12/45 → 13/60/medium
- HeroStat icon plaque: 24px → 28px
- Paused banner title: 14 → 16, hint: 12 → 14/70
- Иконка баннера 36→40px

Settings:
- ToggleRow/SelectRow label: medium → semibold
- Hint: 12/55 → 13/65/medium
- "Конфигурация": 13/45/medium → 14/65/semibold

Exercises/Challenges (row + page):
- Row title: 15/medium → 16/semibold
- Row subtitle: 13/55 → 14/65/medium
- Стат-метрики bold
- Empty state: 14/55 → 15/65/medium
- Warning banner: 13/80 → 14/85/medium + иконка крупнее

Games:
- Game title: 17/semibold → 18/bold
- Install path subtitle: 12/45 → 13/55/medium
- Queue/error banners: 13/80 → 14/85/medium + крупнее иконки и code

ExerciseCard:
- Title: 17/semibold → 18/bold
- Reps meta: 13/55 → 14/65/medium
- Countdown label "Через/Сейчас": 11/45 → 12/60/semibold
- Countdown value: 22/semibold → 24/bold
- "Готово" CTA: 14/semibold → 15/bold, h-10 → h-11

ReminderApp:
- "Время тренировки" label: 12/45 → 13 + accent цвет + bold
- "Раз" подпись: 14/55 → 15/65/semibold
- "Следующее через": 12/45 → 13/65/medium
- Match summary header: 11/45 → 12/65/semibold
- Match summary subtitle: 12/45 → 13/65/medium
- Match summary total: 12/55 → 13/65/medium + 14 → 16 для числа
- ChallengeRow title: 14/medium → 15/semibold
- ChallengeRow subtitle: 12/55 → 13/65/medium
- CTA Готово: 15/semibold → 16/bold

UpdaterCard:
- Cell title medium → semibold
- Cell subtitle: 12/55 → 13/65/medium
- Иконка ячеек 36→40px
- Progress download title medium → semibold + 18px процент

Card SectionHeader:
- 12/medium/45 → 13/semibold/60

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:00:52 +07:00
AnRil
aa60acb164 release(v0.3.2): новые шрифты, иконки сайдбара, light по умолчанию
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled
Release / Build installer + publish release (push) Has been cancelled
== Шрифты ==
- Sans/display: Geist → Manrope (мягче, дружелюбнее, ближе к SF Rounded)
- Serif (hero titles): Instrument Serif → Fraunces с opsz axis 144
- Mono: Geist Mono → JetBrains Mono с ss02/ss19/zero features

== Размеры (iOS HIG calibration) ==
- Hero h1: 40-44px → 32-36px (ближе к настоящему iOS Large Title 34pt)
- Reminder name: 32 → 28; reps counter: 64 → 56
- Match summary title: 22 → 24
- Dashboard stat value: 30 → 26
- Body line-height/letter-spacing подкручены под Manrope

== Иконки сайдбара ==
- LayoutDashboard → Sun (Сегодня — утренняя энергия)
- ListChecks → Dumbbell (Упражнения — спорт прямо)
- Gamepad2 → Joystick (Игры — более игровая иконка)
- Target → Flame (Челленджи — интенсивность)
- Settings → Settings2 (немного объёмнее)
- Размер плашки 28→32px, иконки 15→17px

== Light theme ==
- DEFAULT_SETTINGS.theme: 'system' → 'light' (новые установки сразу
  получают светлую тему)
- Light bg прогрет: 242,242,247 → 245,245,249
- surface-2 чуть темнее для лучшей сепарации полей ввода
- text не pure black а 17,17,19 (легче глазам)
- shadow-card получила тёплый slate-tinge

Существующие пользователи с theme='system' продолжат следовать ОС.
Для принудительного переключения — Settings → Тема → Светлая.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:49:46 +07:00
AnRil
660b6d57d8 chore(release): v0.3.1 — Apple/iOS дизайн
Some checks failed
CI / Typecheck + Tests (push) Has been cancelled
CI / Build (Windows) (push) Has been cancelled
Release / Build installer + publish release (push) Has been cancelled
Включает полный реворк UI в стиле Apple iOS/macOS:
- Geist + Instrument Serif шрифты вместо Rajdhani
- Apple HIG палитра (systemOrange, systemGreen, systemRed, true black dark)
- macOS vibrancy sidebar, iOS grouped lists, UISwitch, action sheets
- Spring анимации, active:scale press feedback

Установщик ведёт себя как install-or-update — обновляет существующую
0.2.x/0.3.0 копию с сохранением настроек.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 16:17:27 +07:00
15 changed files with 166 additions and 157 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "laude", "name": "laude",
"version": "0.3.0", "version": "0.3.3",
"description": "Exercise reminder — Windows desktop app", "description": "Exercise reminder — Windows desktop app",
"main": "out/main/index.js", "main": "out/main/index.js",
"author": "AnRil", "author": "AnRil",

View File

@@ -7,7 +7,7 @@
<title>Exercise Reminder</title> <title>Exercise Reminder</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -139,22 +139,22 @@ function ExerciseReminder({
</div> </div>
</motion.div> </motion.div>
<div className="text-[12px] uppercase tracking-[0.18em] text-text/45 font-medium"> <div className="text-[13px] uppercase tracking-[0.18em] text-accent font-bold">
Время тренировки Время тренировки
</div> </div>
<h1 className="font-serif text-[32px] leading-tight tracking-tight mt-2 mb-3"> <h1 className="font-serif text-[28px] leading-tight tracking-tight mt-2 mb-3 font-medium">
{exercise.name} {exercise.name}
</h1> </h1>
<div className="inline-flex items-baseline gap-2 font-mono-num"> <div className="inline-flex items-baseline gap-2 font-mono-num">
<span className="text-[64px] font-semibold tracking-tight text-text leading-none"> <span className="text-[56px] font-semibold tracking-tight text-text leading-none">
{exercise.reps} {exercise.reps}
</span> </span>
<span className="text-[15px] text-text/55">раз</span> <span className="text-[15px] text-text/65 font-semibold">раз</span>
</div> </div>
<div className="text-[12px] text-text/45 mt-4 inline-flex items-center gap-1.5"> <div className="text-[13px] text-text/65 mt-4 inline-flex items-center gap-1.5 font-medium">
<Clock size={11} strokeWidth={2.4} /> <Clock size={12} strokeWidth={2.4} />
Следующее через {formatInterval(exercise.intervalMinutes)} Следующее через {formatInterval(exercise.intervalMinutes)}
</div> </div>
</div> </div>
@@ -163,20 +163,20 @@ function ExerciseReminder({
<div className="px-4 pb-4 space-y-2"> <div className="px-4 pb-4 space-y-2">
<button <button
onClick={done} onClick={done}
className="w-full h-12 rounded-2xl bg-accent text-white text-[15px] font-semibold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform" className="w-full h-12 rounded-2xl bg-accent text-white text-[16px] font-bold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform"
> >
<Check size={16} strokeWidth={2.5} /> Готово <Check size={17} strokeWidth={2.5} /> Готово
</button> </button>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<button <button
onClick={snooze} onClick={snooze}
className="h-11 rounded-2xl bg-surface-2 text-text text-[14px] font-medium inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform" className="h-11 rounded-2xl bg-surface-2 text-text text-[15px] font-semibold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform"
> >
<Clock size={14} strokeWidth={2.5} /> {snoozeMinutes} мин <Clock size={15} strokeWidth={2.5} /> {snoozeMinutes} мин
</button> </button>
<button <button
onClick={skip} onClick={skip}
className="h-11 rounded-2xl bg-surface-2 text-text/55 text-[14px] font-medium inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform" className="h-11 rounded-2xl bg-surface-2 text-text/65 text-[15px] font-semibold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform"
> >
Пропустить Пропустить
</button> </button>
@@ -208,8 +208,8 @@ function MatchSummaryView({
return ( return (
<div className="reminder-shell flex flex-col h-full"> <div className="reminder-shell flex flex-col h-full">
<div className="titlebar-drag h-9 px-2 flex items-center justify-between"> <div className="titlebar-drag h-9 px-2 flex items-center justify-between">
<div className="text-[11px] text-text/45 font-medium inline-flex items-center gap-1.5 px-2"> <div className="text-[12px] text-text/65 font-semibold inline-flex items-center gap-1.5 px-2">
<Gamepad2 size={11} strokeWidth={2.4} /> {summary.gameName} <Gamepad2 size={12} strokeWidth={2.4} /> {summary.gameName}
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
@@ -238,19 +238,19 @@ function MatchSummaryView({
<Gamepad2 size={26} strokeWidth={2} /> <Gamepad2 size={26} strokeWidth={2} />
)} )}
</motion.div> </motion.div>
<h1 className="font-serif text-[22px] tracking-tight"> <h1 className="font-serif text-[24px] tracking-tight font-medium">
{won ? 'Победа' : lost ? 'Поражение' : 'Матч завершён'} {won ? 'Победа' : lost ? 'Поражение' : 'Матч завершён'}
</h1> </h1>
<p className="text-[12px] text-text/45 mt-1"> <p className="text-[13px] text-text/65 mt-1.5 font-medium">
<span className="font-mono-num font-medium text-text/65"> <span className="font-mono-num font-bold text-text">
{Math.floor(summary.durationMs / 60_000)} {Math.floor(summary.durationMs / 60_000)}
</span>{' '} </span>{' '}
мин · {summary.results.length} челлендж мин · {summary.results.length} челлендж
{summary.results.length === 1 ? '' : 'а'} ·{' '} {summary.results.length === 1 ? '' : 'а'} ·{' '}
{allDone ? ( {allDone ? (
<span className="text-success font-medium">всё готово</span> <span className="text-success font-bold">всё готово</span>
) : ( ) : (
<span className="text-accent font-mono-num font-semibold"> <span className="text-accent font-mono-num font-bold">
{remainingReps} осталось {remainingReps} осталось
</span> </span>
)} )}
@@ -269,9 +269,9 @@ function MatchSummaryView({
</div> </div>
<div className="px-4 pb-4 pt-3 flex items-center gap-3"> <div className="px-4 pb-4 pt-3 flex items-center gap-3">
<div className="flex-1 text-[12px] text-text/55"> <div className="flex-1 text-[13px] text-text/65 font-medium">
Всего ·{' '} Всего ·{' '}
<span className="text-text font-mono-num font-semibold text-[14px]"> <span className="text-text font-mono-num font-bold text-[16px]">
{totalReps} {totalReps}
</span>{' '} </span>{' '}
повторов повторов
@@ -326,14 +326,14 @@ function ChallengeRow({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div <div
className={[ className={[
'text-[14px] font-medium truncate', 'text-[15px] font-semibold truncate',
done ? 'line-through opacity-55' : '' done ? 'line-through opacity-55' : ''
].join(' ')} ].join(' ')}
> >
{result.exerciseName} {result.exerciseName}
</div> </div>
<div className="text-[12px] text-text/55 mt-0.5"> <div className="text-[13px] text-text/65 mt-0.5 font-medium">
<span className="font-mono-num font-semibold text-text/75"> <span className="font-mono-num font-bold text-text">
{result.statValue} {result.statValue}
</span>{' '} </span>{' '}
{result.statLabel} <span>{result.name}</span> {result.statLabel} <span>{result.name}</span>

View File

@@ -97,7 +97,7 @@ export function ExerciseCard({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h3 className="font-display text-[17px] font-semibold leading-tight truncate"> <h3 className="font-display text-[18px] font-bold leading-tight truncate">
{exercise.name} {exercise.name}
</h3> </h3>
<div className="relative"> <div className="relative">
@@ -138,19 +138,19 @@ export function ExerciseCard({
)} )}
</div> </div>
</div> </div>
<div className="text-[13px] text-text/55 mt-0.5"> <div className="text-[14px] text-text/65 mt-1 font-medium">
{exercise.reps} раз · каждые {formatInterval(exercise.intervalMinutes)} {exercise.reps} раз · каждые {formatInterval(exercise.intervalMinutes)}
</div> </div>
{/* Countdown + switch */} {/* Countdown + switch */}
<div className="flex items-end justify-between mt-3.5"> <div className="flex items-end justify-between mt-3.5">
<div> <div>
<div className="text-[11px] text-text/45 uppercase tracking-wider font-medium"> <div className="text-[12px] text-text/60 uppercase tracking-wider font-semibold">
{isDue ? 'Сейчас' : 'Через'} {isDue ? 'Сейчас' : 'Через'}
</div> </div>
<div <div
className={[ className={[
'font-mono-num text-[22px] font-semibold leading-none mt-0.5 tracking-tight', 'font-mono-num text-[24px] font-bold leading-none mt-1 tracking-tight',
isDue ? 'text-accent' : 'text-text' isDue ? 'text-accent' : 'text-text'
].join(' ')} ].join(' ')}
> >
@@ -172,7 +172,7 @@ export function ExerciseCard({
initial={{ opacity: 0, y: 4 }} initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
onClick={onMarkDone} onClick={onMarkDone}
className="mt-4 w-full h-10 rounded-xl bg-accent text-white text-[14px] font-semibold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform" className="mt-4 w-full h-11 rounded-xl bg-accent text-white text-[15px] font-bold inline-flex items-center justify-center gap-1.5 active:scale-[0.98] transition-transform"
> >
<Check size={15} strokeWidth={2.5} /> Готово <Check size={15} strokeWidth={2.5} /> Готово
</motion.button> </motion.button>

View File

@@ -1,38 +1,37 @@
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { AnimatePresence, motion } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion'
import { import {
LayoutDashboard, Sun,
ListChecks, Dumbbell,
Gamepad2, Joystick,
Target, Flame,
Settings as SettingsIcon, Settings2,
X X
} from 'lucide-react' } from 'lucide-react'
type Item = { type Item = {
to: string to: string
label: string label: string
icon: typeof LayoutDashboard icon: typeof Sun
end?: boolean end?: boolean
tint?: string tint?: string
} }
// Each item gets a tinted icon-square reminiscent of iOS Settings rows. // Tinted icon plaques á la iOS Settings rows.
const items: Item[] = [ const items: Item[] = [
{ to: '/', label: 'Сегодня', icon: Sun, end: true, tint: 'bg-accent' },
{ {
to: '/', to: '/exercises',
label: 'Сегодня', label: 'Упражнения',
icon: LayoutDashboard, icon: Dumbbell,
end: true, tint: 'bg-info'
tint: 'bg-accent'
}, },
{ to: '/exercises', label: 'Упражнения', icon: ListChecks, tint: 'bg-info' }, { to: '/games', label: 'Игры', icon: Joystick, tint: 'bg-accent-2' },
{ to: '/games', label: 'Игры', icon: Gamepad2, tint: 'bg-accent-2' }, { to: '/challenges', label: 'Челленджи', icon: Flame, tint: 'bg-warning' },
{ to: '/challenges', label: 'Челленджи', icon: Target, tint: 'bg-warning' },
{ {
to: '/settings', to: '/settings',
label: 'Настройки', label: 'Настройки',
icon: SettingsIcon, icon: Settings2,
tint: 'bg-text/70' tint: 'bg-text/70'
} }
] ]
@@ -98,16 +97,16 @@ function SidebarContent({ onNav }: { onNav?: () => void }): JSX.Element {
<> <>
{/* Brand */} {/* Brand */}
<div className="px-5 pt-7 pb-6"> <div className="px-5 pt-7 pb-6">
<div className="font-serif text-[28px] leading-none tracking-tight"> <div className="font-serif text-[34px] leading-none tracking-tight font-medium">
Laude Laude
</div> </div>
<div className="text-[12px] text-text/45 mt-1.5"> <div className="text-[12px] text-text/45 mt-2 tracking-tight">
Move with intention Двигайся осознанно
</div> </div>
</div> </div>
{/* Nav */} {/* Nav */}
<nav className="px-2.5 flex flex-col gap-0.5"> <nav className="px-2.5 flex flex-col gap-1">
{items.map(({ to, label, icon: Icon, end, tint }) => ( {items.map(({ to, label, icon: Icon, end, tint }) => (
<NavLink <NavLink
key={to} key={to}
@@ -127,15 +126,15 @@ function SidebarContent({ onNav }: { onNav?: () => void }): JSX.Element {
<> <>
<div <div
className={[ className={[
'w-7 h-7 rounded-lg grid place-items-center text-white shrink-0', 'w-8 h-8 rounded-[9px] grid place-items-center text-white shrink-0',
tint ?? 'bg-text/70' tint ?? 'bg-text/70'
].join(' ')} ].join(' ')}
> >
<Icon size={15} strokeWidth={2.4} /> <Icon size={17} strokeWidth={2.2} />
</div> </div>
<span <span
className={[ className={[
'text-[14px] truncate', 'text-[15px] truncate',
isActive isActive
? 'text-text font-semibold' ? 'text-text font-semibold'
: 'text-text/85 font-medium' : 'text-text/85 font-medium'

View File

@@ -126,19 +126,19 @@ function Body({
return ( return (
<div className="px-4 py-4"> <div className="px-4 py-4">
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<div className="w-9 h-9 rounded-xl bg-accent/12 text-accent grid place-items-center"> <div className="w-10 h-10 rounded-xl bg-accent/12 text-accent grid place-items-center">
<Download size={16} strokeWidth={2.4} /> <Download size={17} strokeWidth={2.4} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[15px] font-medium leading-tight"> <div className="text-[15px] font-semibold leading-tight">
Загружаем обновление Загружаем обновление
</div> </div>
<div className="text-[12px] text-text/55 mt-0.5 font-mono-num"> <div className="text-[13px] text-text/65 mt-1 font-mono-num font-medium">
{mb(status.transferred)} / {mb(status.total)} МБ ·{' '} {mb(status.transferred)} / {mb(status.total)} МБ ·{' '}
{(status.bytesPerSecond / 1024 / 1024).toFixed(2)} МБ/с {(status.bytesPerSecond / 1024 / 1024).toFixed(2)} МБ/с
</div> </div>
</div> </div>
<div className="font-mono-num font-semibold text-[17px] text-accent"> <div className="font-mono-num font-bold text-[18px] text-accent">
{pct.toFixed(0)}% {pct.toFixed(0)}%
</div> </div>
</div> </div>
@@ -218,19 +218,19 @@ function Cell({
muted: 'bg-surface-2 text-text/55' muted: 'bg-surface-2 text-text/55'
}[tone] }[tone]
return ( return (
<div className="flex items-center gap-3 px-4 py-3.5"> <div className="flex items-center gap-3 px-4 py-4">
<div <div
className={[ className={[
'w-9 h-9 rounded-xl grid place-items-center shrink-0', 'w-10 h-10 rounded-xl grid place-items-center shrink-0',
cls cls
].join(' ')} ].join(' ')}
> >
{icon} {icon}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[15px] font-medium leading-tight">{title}</div> <div className="text-[15px] font-semibold leading-tight">{title}</div>
{subtitle && ( {subtitle && (
<div className="text-[12px] text-text/55 mt-0.5 truncate"> <div className="text-[13px] text-text/65 mt-1 truncate font-medium">
{subtitle} {subtitle}
</div> </div>
)} )}

View File

@@ -39,12 +39,16 @@ export function SectionHeader({
action?: ReactNode action?: ReactNode
}): JSX.Element { }): JSX.Element {
return ( return (
<div className="flex items-end justify-between px-4 mb-2"> <div className="flex items-end justify-between px-4 mb-2.5">
<div> <div>
<div className="text-[12px] font-medium uppercase tracking-[0.06em] text-text/45"> <div className="text-[13px] font-semibold uppercase tracking-[0.06em] text-text/60">
{title} {title}
</div> </div>
{hint && <div className="text-[12px] text-text/45 mt-0.5">{hint}</div>} {hint && (
<div className="text-[13px] text-text/55 mt-0.5 font-medium">
{hint}
</div>
)}
</div> </div>
{action} {action}
</div> </div>

View File

@@ -48,14 +48,14 @@ export default function ChallengesPage(): JSX.Element {
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div> <div>
<div className="text-[13px] text-text/45 font-medium"> <div className="text-[14px] text-text/65 font-semibold">
Правила за матч Правила за матч
</div> </div>
<h1 className="font-serif text-[40px] sm:text-[44px] leading-[1.05] tracking-tight mt-1"> <h1 className="font-serif text-[32px] sm:text-[36px] leading-[1.05] tracking-tight mt-1 font-medium">
Челленджи Челленджи
</h1> </h1>
<p className="text-[14px] text-text/55 mt-2"> <p className="text-[15px] text-text/65 mt-2 font-medium">
Повторов = <span className="font-mono-num">статистика × коэффициент</span> Повторов = <span className="font-mono-num font-semibold text-text">статистика × коэффициент</span>
</p> </p>
</div> </div>
<Button <Button
@@ -70,12 +70,12 @@ export default function ChallengesPage(): JSX.Element {
{noGamesActive && ( {noGamesActive && (
<div className="mb-6 rounded-2xl bg-warning/12 p-4 flex items-start gap-3"> <div className="mb-6 rounded-2xl bg-warning/12 p-4 flex items-start gap-3">
<div className="w-9 h-9 rounded-xl bg-warning/15 text-warning grid place-items-center shrink-0"> <div className="w-10 h-10 rounded-xl bg-warning/18 text-warning grid place-items-center shrink-0">
<AlertTriangle size={16} strokeWidth={2.5} /> <AlertTriangle size={18} strokeWidth={2.5} />
</div> </div>
<div className="text-[13px] text-text/80 leading-relaxed"> <div className="text-[14px] text-text/85 leading-relaxed font-medium">
Челленджи срабатывают после матча. Подключи игру во вкладке{' '} Челленджи срабатывают после матча. Подключи игру во вкладке{' '}
<span className="font-semibold">«Игры»</span>. <span className="font-semibold text-text">«Игры»</span>.
</div> </div>
</div> </div>
)} )}
@@ -104,13 +104,13 @@ export default function ChallengesPage(): JSX.Element {
<Icon name={c.icon} size={18} strokeWidth={2.2} /> <Icon name={c.icon} size={18} strokeWidth={2.2} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[15px] font-medium truncate leading-tight"> <div className="text-[16px] font-semibold truncate leading-tight">
{c.name} {c.name}
</div> </div>
<div className="text-[13px] text-text/55 mt-0.5 inline-flex items-center gap-1.5"> <div className="text-[14px] text-text/65 mt-1 inline-flex items-center gap-1.5 font-medium">
<Gamepad2 size={11} /> <Gamepad2 size={12} strokeWidth={2.4} />
{GAME_NAMES[c.gameId]} ·{' '} {GAME_NAMES[c.gameId]} ·{' '}
<span className="font-mono-num"> <span className="font-mono-num font-semibold text-text">
{STAT_LABELS[c.stat]} × {c.multiplier} {STAT_LABELS[c.stat]} × {c.multiplier}
</span>{' '} </span>{' '}
{c.exerciseName} {c.exerciseName}

View File

@@ -69,10 +69,10 @@ export default function Dashboard(): JSX.Element {
{/* Hero — iOS Large Title */} {/* Hero — iOS Large Title */}
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div className="min-w-0"> <div className="min-w-0">
<div className="text-[13px] text-text/45 font-medium capitalize"> <div className="text-[14px] text-text/65 font-semibold capitalize">
{today} {today}
</div> </div>
<h1 className="font-serif text-[40px] sm:text-[44px] leading-[1.05] tracking-tight mt-1"> <h1 className="font-serif text-[32px] sm:text-[36px] leading-[1.05] tracking-tight mt-1 font-medium">
Сегодня Сегодня
</h1> </h1>
</div> </div>
@@ -139,19 +139,19 @@ export default function Dashboard(): JSX.Element {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="mb-6 rounded-2xl bg-warning/12 p-4 flex items-center gap-3" className="mb-6 rounded-2xl bg-warning/12 p-4 flex items-center gap-3"
> >
<div className="w-9 h-9 rounded-xl bg-warning/15 text-warning grid place-items-center"> <div className="w-10 h-10 rounded-xl bg-warning/18 text-warning grid place-items-center shrink-0">
<Pause size={16} strokeWidth={2.5} /> <Pause size={18} strokeWidth={2.5} />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[14px] font-semibold"> <div className="text-[16px] font-semibold leading-tight">
Напоминания на паузе Напоминания на паузе
</div> </div>
<div className="text-[12px] text-text/55 mt-0.5"> <div className="text-[14px] text-text/70 mt-1">
Возобнови, чтобы продолжить отсчёт Возобнови, чтобы продолжить отсчёт
</div> </div>
</div> </div>
<Button variant="filled" size="sm" onClick={togglePause}> <Button variant="filled" size="sm" onClick={togglePause}>
<Play size={13} strokeWidth={2.5} /> Старт <Play size={14} strokeWidth={2.5} /> Старт
</Button> </Button>
</motion.div> </motion.div>
)} )}
@@ -225,19 +225,21 @@ function HeroStat({
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<div <div
className={[ className={[
'w-6 h-6 rounded-md grid place-items-center text-white', 'w-7 h-7 rounded-lg grid place-items-center text-white',
toneBg toneBg
].join(' ')} ].join(' ')}
> >
{icon} {icon}
</div> </div>
<div className="text-[12px] text-text/55 font-medium">{label}</div> <div className="text-[14px] text-text/75 font-semibold">{label}</div>
</div> </div>
<div className="font-display text-[30px] font-semibold tracking-tight leading-none"> <div className="font-display text-[28px] font-bold tracking-tight leading-none">
{value} {value}
</div> </div>
{subvalue && ( {subvalue && (
<div className="text-[12px] text-text/45 mt-1.5">{subvalue}</div> <div className="text-[13px] text-text/60 mt-2 font-medium">
{subvalue}
</div>
)} )}
</div> </div>
) )

View File

@@ -22,10 +22,10 @@ export default function Exercises(): JSX.Element {
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div> <div>
<div className="text-[13px] text-text/45 font-medium"> <div className="text-[14px] text-text/65 font-semibold">
Программа Программа
</div> </div>
<h1 className="font-serif text-[40px] sm:text-[44px] leading-[1.05] tracking-tight mt-1"> <h1 className="font-serif text-[32px] sm:text-[36px] leading-[1.05] tracking-tight mt-1 font-medium">
Упражнения Упражнения
</h1> </h1>
</div> </div>
@@ -79,7 +79,7 @@ export default function Exercises(): JSX.Element {
{exercises.length === 0 && ( {exercises.length === 0 && (
<Card> <Card>
<div className="px-5 py-12 text-center text-text/55 text-[14px]"> <div className="px-5 py-12 text-center text-text/65 text-[15px] font-medium">
Программа пуста добавь первое упражнение Программа пуста добавь первое упражнение
</div> </div>
</Card> </Card>
@@ -126,10 +126,10 @@ function ExerciseRow({
onClick={onEdit} onClick={onEdit}
className="flex-1 min-w-0 text-left active:opacity-70 transition-opacity" className="flex-1 min-w-0 text-left active:opacity-70 transition-opacity"
> >
<div className="text-[15px] font-medium truncate leading-tight"> <div className="text-[16px] font-semibold truncate leading-tight">
{exercise.name} {exercise.name}
</div> </div>
<div className="text-[13px] text-text/55 mt-0.5"> <div className="text-[14px] text-text/65 mt-1 font-medium">
{exercise.reps} раз · {formatInterval(exercise.intervalMinutes)} {exercise.reps} раз · {formatInterval(exercise.intervalMinutes)}
</div> </div>
</button> </button>

View File

@@ -61,18 +61,18 @@ export default function GamesPage(): JSX.Element {
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12">
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4 mb-8">
<div> <div>
<div className="text-[13px] text-text/45 font-medium"> <div className="text-[14px] text-text/65 font-semibold">
Трекинг матчей Трекинг матчей
</div> </div>
<h1 className="font-serif text-[40px] sm:text-[44px] leading-[1.05] tracking-tight mt-1"> <h1 className="font-serif text-[32px] sm:text-[36px] leading-[1.05] tracking-tight mt-1 font-medium">
Игры Игры
</h1> </h1>
<p className="text-[14px] text-text/55 mt-2"> <p className="text-[15px] text-text/65 mt-2 font-medium leading-relaxed">
Подключи игру челленджи сработают сразу после матча Подключи игру челленджи сработают сразу после матча
{liveCount > 0 && ( {liveCount > 0 && (
<> <>
{' · '} {' · '}
<span className="text-success font-mono-num font-semibold"> <span className="text-success font-mono-num font-bold">
{liveCount} live {liveCount} live
</span> </span>
</> </>
@@ -154,13 +154,13 @@ function GameCard({
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="font-display text-[17px] font-semibold tracking-tight"> <h3 className="font-display text-[18px] font-bold tracking-tight">
{game.name} {game.name}
</h3> </h3>
<StatusBadge game={game} isLive={isLive} /> <StatusBadge game={game} isLive={isLive} />
</div> </div>
{game.installPath && ( {game.installPath && (
<div className="text-[12px] text-text/45 mt-1 truncate font-mono-num"> <div className="text-[13px] text-text/55 mt-1.5 truncate font-mono-num font-medium">
{game.installPath} {game.installPath}
</div> </div>
)} )}
@@ -176,15 +176,15 @@ function GameCard({
</div> </div>
{game.integrationActive && game.launchOptionStatus === 'queued' && ( {game.integrationActive && game.launchOptionStatus === 'queued' && (
<div className="mt-4 rounded-2xl bg-warning/12 p-3.5 text-[13px] leading-relaxed flex items-start gap-2.5"> <div className="mt-4 rounded-2xl bg-warning/12 p-4 text-[14px] leading-relaxed flex items-start gap-2.5 font-medium">
<Hourglass <Hourglass
size={15} size={17}
className="text-warning shrink-0 mt-0.5" className="text-warning shrink-0 mt-0.5"
strokeWidth={2.4} strokeWidth={2.4}
/> />
<div className="text-text/80"> <div className="text-text/85">
Steam запущен. Параметр{' '} Steam запущен. Параметр{' '}
<code className="px-1.5 py-0.5 rounded-md bg-surface text-accent font-mono-num text-[12px]"> <code className="px-1.5 py-0.5 rounded-md bg-surface text-accent font-mono-num text-[13px] font-semibold">
{game.launchOption} {game.launchOption}
</code>{' '} </code>{' '}
пропишется автоматически при следующем закрытии Steam. пропишется автоматически при следующем закрытии Steam.
@@ -193,15 +193,15 @@ function GameCard({
)} )}
{game.integrationActive && game.launchOptionStatus === 'no_user' && ( {game.integrationActive && game.launchOptionStatus === 'no_user' && (
<div className="mt-4 rounded-2xl bg-destructive/10 p-3.5 text-[13px] leading-relaxed flex items-start gap-2.5"> <div className="mt-4 rounded-2xl bg-destructive/10 p-4 text-[14px] leading-relaxed flex items-start gap-2.5 font-medium">
<AlertTriangle <AlertTriangle
size={15} size={17}
className="text-destructive shrink-0 mt-0.5" className="text-destructive shrink-0 mt-0.5"
strokeWidth={2.4} strokeWidth={2.4}
/> />
<div className="text-text/80"> <div className="text-text/85">
В Steam нет залогиненного аккаунта (нет папки{' '} В Steam нет залогиненного аккаунта (нет папки{' '}
<code className="font-mono-num text-[12px]">userdata</code>). <code className="font-mono-num text-[13px] font-semibold">userdata</code>).
Запусти Steam один раз и нажми «Установить интеграцию». Запусти Steam один раз и нажми «Установить интеграцию».
</div> </div>
</div> </div>
@@ -224,7 +224,7 @@ function GameCard({
</Button> </Button>
)} )}
{!game.installed && ( {!game.installed && (
<div className="text-[13px] text-text/55"> <div className="text-[14px] text-text/65 font-medium">
Установи игру в Steam и нажми «Обновить» Установи игру в Steam и нажми «Обновить»
</div> </div>
)} )}

View File

@@ -21,10 +21,10 @@ export default function SettingsPage(): JSX.Element {
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12"> <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-10 pt-8 pb-12">
<div className="mb-8"> <div className="mb-8">
<div className="text-[13px] text-text/45 font-medium"> <div className="text-[14px] text-text/65 font-semibold">
Конфигурация Конфигурация
</div> </div>
<h1 className="font-serif text-[40px] sm:text-[44px] leading-[1.05] tracking-tight mt-1"> <h1 className="font-serif text-[32px] sm:text-[36px] leading-[1.05] tracking-tight mt-1 font-medium">
Настройки Настройки
</h1> </h1>
</div> </div>
@@ -132,9 +132,11 @@ function ToggleRow({
return ( return (
<Row last={last} className={disabled ? 'opacity-50' : ''}> <Row last={last} className={disabled ? 'opacity-50' : ''}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[15px] font-medium leading-tight">{label}</div> <div className="text-[15px] font-semibold leading-tight">{label}</div>
{hint && ( {hint && (
<div className="text-[12px] text-text/55 mt-0.5">{hint}</div> <div className="text-[13px] text-text/65 mt-1 leading-snug">
{hint}
</div>
)} )}
</div> </div>
<Switch checked={checked} onChange={onChange} disabled={disabled} /> <Switch checked={checked} onChange={onChange} disabled={disabled} />
@@ -160,9 +162,11 @@ function SelectRow({
return ( return (
<Row last={last}> <Row last={last}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-[15px] font-medium leading-tight">{label}</div> <div className="text-[15px] font-semibold leading-tight">{label}</div>
{hint && ( {hint && (
<div className="text-[12px] text-text/55 mt-0.5">{hint}</div> <div className="text-[13px] text-text/65 mt-1 leading-snug">
{hint}
</div>
)} )}
</div> </div>
<select <select

View File

@@ -2,34 +2,33 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* ===== Design tokens — Apple Human Interface Guidelines ===== */ /* ===== Design tokens — Apple HIG with Manrope/Fraunces ===== */
:root { :root {
/* Brand & semantic colors (iOS system palette) */ /* Brand & semantic colors (iOS system palette) */
--accent: 255 107 53; /* Apple Fitness Move orange */ --accent: 255 107 53; /* Apple Fitness Move orange */
--accent-2: 255 45 85; /* systemPink */ --accent-2: 255 45 85; /* systemPink */
--success: 52 199 89; /* systemGreen */ --success: 52 199 89; /* systemGreen */
--warning: 255 159 10; /* systemOrange-dark */ --warning: 255 159 10; /* systemOrange dark */
--destructive: 255 59 48; /* systemRed */ --destructive: 255 59 48; /* systemRed */
--info: 0 122 255; /* systemBlue */ --info: 0 122 255; /* systemBlue */
color-scheme: light dark; color-scheme: light dark;
} }
/* Light — iOS groupedBackground feel */ /* Light — polished iOS groupedBackground with warm undertone */
:root { :root {
--bg: 242 242 247; /* systemGroupedBackground */ --bg: 245 245 249; /* slightly warmer than 242,242,247 */
--surface: 255 255 255; /* secondarySystemGroupedBackground (cards) */ --surface: 255 255 255;
--surface-2: 242 242 247; /* tertiarySystemGroupedBackground */ --surface-2: 240 240 245; /* subtle separation for inputs/sections */
--text: 0 0 0; --text: 17 17 19; /* not pure black — softer */
--text-secondary: 60 60 67; /* used with opacity 0.6 */ --text-secondary: 60 60 67;
--text-tertiary: 60 60 67; /* used with opacity 0.3 */ --text-tertiary: 60 60 67;
--hairline: 60 60 67; /* used with opacity 0.18 */ --hairline: 60 60 67;
--vibrancy: 255 255 255; /* sidebar translucent base */ --vibrancy: 255 255 255;
/* Legacy tokens (mapped to keep some old utility classes working) */
--accent-soft: 255 107 53; --accent-soft: 255 107 53;
--bg-deep: 230 230 235; --bg-deep: 232 232 238;
--surface-elevated: 255 255 255; --surface-elevated: 255 255 255;
--border: 60 60 67; --border: 60 60 67;
--muted: 60 60 67; --muted: 60 60 67;
@@ -38,11 +37,11 @@
--xp: 255 159 10; --xp: 255 159 10;
} }
/* Dark — iOS true black for OLED, elevation via grey steps */ /* Dark — true black with grey elevation */
.dark { .dark {
--bg: 0 0 0; /* systemBackground */ --bg: 0 0 0;
--surface: 28 28 30; /* secondarySystemBackground */ --surface: 28 28 30;
--surface-2: 44 44 46; /* tertiarySystemBackground */ --surface-2: 44 44 46;
--text: 255 255 255; --text: 255 255 255;
--text-secondary: 235 235 245; --text-secondary: 235 235 245;
--text-tertiary: 235 235 245; --text-tertiary: 235 235 245;
@@ -68,24 +67,26 @@ body,
body { body {
font-family: font-family:
'Geist', 'Manrope',
-apple-system, -apple-system,
'SF Pro Text', 'SF Pro Text',
'Segoe UI Variable Text', 'Segoe UI Variable Text',
'Segoe UI', 'Segoe UI',
system-ui, system-ui,
sans-serif; sans-serif;
font-feature-settings: 'cv11', 'ss01', 'ss03'; /* Geist stylistic alts */
background-color: rgb(var(--bg)); background-color: rgb(var(--bg));
color: rgb(var(--text)); color: rgb(var(--text));
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
letter-spacing: -0.01em; font-size: 14px;
line-height: 1.45;
letter-spacing: -0.005em;
} }
/* Display — same Manrope but slightly tighter for headings */
.font-display { .font-display {
font-family: font-family:
'Geist', 'Manrope',
-apple-system, -apple-system,
'SF Pro Display', 'SF Pro Display',
'Segoe UI Variable Display', 'Segoe UI Variable Display',
@@ -94,17 +95,20 @@ body {
letter-spacing: -0.02em; letter-spacing: -0.02em;
} }
/* Serif — Fraunces for hero titles with optical-size axis */
.font-serif { .font-serif {
font-family: 'Instrument Serif', 'Iowan Old Style', 'Apple Garamond', Georgia, font-family: 'Fraunces', 'Iowan Old Style', 'New York', Georgia, serif;
serif; font-optical-sizing: auto;
letter-spacing: -0.01em; font-variation-settings: 'opsz' 144;
letter-spacing: -0.025em;
} }
.font-mono-num { .font-mono-num {
font-family: 'Geist Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code',
monospace; Menlo, monospace;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-feature-settings: 'ss02'; font-feature-settings: 'ss02', 'ss19', 'zero';
letter-spacing: -0.01em;
} }
/* Custom titlebar drag region */ /* Custom titlebar drag region */
@@ -117,7 +121,7 @@ body {
app-region: no-drag; app-region: no-drag;
} }
/* Thin iOS-style hairline (effectively 0.5px when device DPR allows) */ /* iOS 0.5px-style hairlines */
.hairline-b { .hairline-b {
box-shadow: inset 0 -0.5px 0 rgb(var(--hairline) / 0.18); box-shadow: inset 0 -0.5px 0 rgb(var(--hairline) / 0.18);
} }
@@ -129,19 +133,19 @@ body {
box-shadow: inset 0 -0.5px 0 rgb(var(--hairline) / 0.4); box-shadow: inset 0 -0.5px 0 rgb(var(--hairline) / 0.4);
} }
/* Vibrancy panel (macOS Big Sur+ sidebar feel) */ /* macOS vibrancy */
.vibrancy { .vibrancy {
background-color: rgb(var(--vibrancy) / 0.72); background-color: rgb(var(--vibrancy) / 0.72);
backdrop-filter: saturate(180%) blur(30px); backdrop-filter: saturate(180%) blur(30px);
-webkit-backdrop-filter: saturate(180%) blur(30px); -webkit-backdrop-filter: saturate(180%) blur(30px);
} }
/* iOS-style soft card shadow */ /* Soft iOS card shadow with subtle warmth */
.shadow-card { .shadow-card {
box-shadow: box-shadow:
0 0.5px 0 rgb(0 0 0 / 0.03), 0 0.5px 0 rgb(0 0 0 / 0.03),
0 1px 2px rgb(0 0 0 / 0.04), 0 1px 2px rgb(15 23 42 / 0.04),
0 4px 12px rgb(0 0 0 / 0.04); 0 6px 14px -4px rgb(15 23 42 / 0.05);
} }
.dark .shadow-card { .dark .shadow-card {
box-shadow: box-shadow:
@@ -149,7 +153,7 @@ body {
0 1px 2px rgb(0 0 0 / 0.4); 0 1px 2px rgb(0 0 0 / 0.4);
} }
/* Scrollbar — thin, iOS-style */ /* Scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
@@ -165,18 +169,15 @@ body {
background: transparent; background: transparent;
} }
/* Selection */
::selection { ::selection {
background: rgb(var(--accent) / 0.25); background: rgb(var(--accent) / 0.25);
} }
/* iOS focus ring */
*:focus-visible { *:focus-visible {
outline: 2px solid rgb(var(--accent) / 0.55); outline: 2px solid rgb(var(--accent) / 0.55);
outline-offset: 2px; outline-offset: 2px;
} }
/* Reminder window root — iOS sheet */
.reminder-shell { .reminder-shell {
position: relative; position: relative;
border: 0.5px solid rgb(var(--hairline) / 0.25); border: 0.5px solid rgb(var(--hairline) / 0.25);
@@ -189,7 +190,6 @@ body {
height: 100%; height: 100%;
} }
/* Text helpers (semantic aliases) */
.text-secondary { .text-secondary {
color: rgb(var(--text-secondary) / 0.6); color: rgb(var(--text-secondary) / 0.6);
} }

View File

@@ -121,7 +121,7 @@ export const DEFAULT_SETTINGS: Settings = {
startWithWindows: false, startWithWindows: false,
minimizeToTray: true, minimizeToTray: true,
startMinimized: false, startMinimized: false,
theme: 'system', theme: 'light',
snoozeMinutes: 5 snoozeMinutes: 5
} }

View File

@@ -34,7 +34,7 @@ export default {
}, },
fontFamily: { fontFamily: {
sans: [ sans: [
'Geist', 'Manrope',
'-apple-system', '-apple-system',
'SF Pro Text', 'SF Pro Text',
'Segoe UI Variable Text', 'Segoe UI Variable Text',
@@ -43,7 +43,7 @@ export default {
'sans-serif' 'sans-serif'
], ],
display: [ display: [
'Geist', 'Manrope',
'-apple-system', '-apple-system',
'SF Pro Display', 'SF Pro Display',
'Segoe UI Variable Display', 'Segoe UI Variable Display',
@@ -51,14 +51,14 @@ export default {
'sans-serif' 'sans-serif'
], ],
serif: [ serif: [
'Instrument Serif', 'Fraunces',
'Iowan Old Style', 'Iowan Old Style',
'Apple Garamond', 'New York',
'Georgia', 'Georgia',
'serif' 'serif'
], ],
mono: [ mono: [
'Geist Mono', 'JetBrains Mono',
'ui-monospace', 'ui-monospace',
'SF Mono', 'SF Mono',
'Cascadia Code', 'Cascadia Code',