rebuild theme system and restyle ui

flatten themes to 3 colors without categories, rewrite css in vanilla layers,
simplify onboarding flow, tighten greeting punctuation, adjust viewport meta
This commit is contained in:
Ned Halksworth
2026-05-27 17:31:00 +01:00
committed by Ned
parent cbdd98e133
commit add2586cb1
7 changed files with 1466 additions and 1062 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta <meta
name="description" name="description"
content="A local-first Red Bull intake web app for tracking cans, spending, caffeine, sugar, flavours, and trends." content="A local-first Red Bull intake web app for tracking cans, spending, caffeine, sugar, flavours, and trends."
+3 -3
View File
@@ -15,8 +15,8 @@ export function DailyLimitsCard({ limits, check, onOpenSettings }: DailyLimitsCa
<section className="limits-card glass-panel p-5 sm:p-6"> <section className="limits-card glass-panel p-5 sm:p-6">
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div> <div>
<p className="text-sm font-medium uppercase tracking-[0.18em] text-cyan-100">Daily limits</p> <p className="section-kicker">Daily limits</p>
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-400"> <p className="section-meta mt-2 max-w-xl leading-6">
Set how many cans you want per day, when to stop, and a spend cap. Limits are optional and stored on your Set how many cans you want per day, when to stop, and a spend cap. Limits are optional and stored on your
account. account.
</p> </p>
@@ -37,7 +37,7 @@ export function DailyLimitsCard({ limits, check, onOpenSettings }: DailyLimitsCa
return ( return (
<section className="limits-card glass-panel p-5 sm:p-6"> <section className="limits-card glass-panel p-5 sm:p-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<p className="text-sm font-medium uppercase tracking-[0.18em] text-cyan-100">Daily limits</p> <p className="section-kicker">Daily limits</p>
<button className="list-button !min-h-9 !px-3 !py-1.5 text-xs" type="button" onClick={onOpenSettings}> <button className="list-button !min-h-9 !px-3 !py-1.5 text-xs" type="button" onClick={onOpenSettings}>
<Settings2 size={14} aria-hidden="true" /> <Settings2 size={14} aria-hidden="true" />
Edit Edit
+4 -4
View File
@@ -56,7 +56,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
<form className="grid gap-4" onSubmit={submit}> <form className="grid gap-4" onSubmit={submit}>
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<label className="grid gap-2 text-sm"> <label className="grid gap-2 text-sm">
<span className="font-medium text-slate-300">Cans per day</span> <span className="font-medium text-slate-700">Cans per day</span>
<input <input
className="field-input" className="field-input"
type="number" type="number"
@@ -70,7 +70,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
</label> </label>
<label className="grid gap-2 text-sm"> <label className="grid gap-2 text-sm">
<span className="font-medium text-slate-300">Spend per day (£)</span> <span className="font-medium text-slate-700">Spend per day (£)</span>
<input <input
className="field-input" className="field-input"
type="number" type="number"
@@ -85,7 +85,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
</div> </div>
<label className="grid gap-2 text-sm sm:max-w-xs"> <label className="grid gap-2 text-sm sm:max-w-xs">
<span className="font-medium text-slate-300">Stop drinking by</span> <span className="font-medium text-slate-700">Stop drinking by</span>
<input <input
className="field-input" className="field-input"
type="time" type="time"
@@ -96,7 +96,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
</label> </label>
{previewParts.length ? ( {previewParts.length ? (
<p className="rounded-lg border border-white/10 bg-white/[0.05] px-3 py-2 text-sm text-slate-300"> <p className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700">
Today so far: {previewParts.join(" · ")} Today so far: {previewParts.join(" · ")}
</p> </p>
) : null} ) : null}
+19 -46
View File
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { ArrowRight, Check, ChevronLeft } from "lucide-react"; import { ArrowRight, Check, ChevronLeft } from "lucide-react";
import { APP_THEMES, THEME_CATEGORIES, type ThemeCategory } from "../data/themes"; import { APP_THEMES } from "../data/themes";
import { currency } from "../lib/metrics"; import { currency } from "../lib/metrics";
import type { UserLimits } from "../types"; import type { UserLimits } from "../types";
@@ -33,12 +33,6 @@ export function OnboardingScreen({
const [dailySpendLimit, setDailySpendLimit] = useState<number | "none">(3.5); const [dailySpendLimit, setDailySpendLimit] = useState<number | "none">(3.5);
const [stopTime, setStopTime] = useState<string | "none">("18:00"); const [stopTime, setStopTime] = useState<string | "none">("18:00");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [activeCategory, setActiveCategory] = useState<ThemeCategory>("flavour");
const visibleThemes = useMemo(() => {
return APP_THEMES.filter((theme) => theme.category === activeCategory);
}, [activeCategory]);
const activeTheme = useMemo(() => { const activeTheme = useMemo(() => {
return APP_THEMES.find((theme) => theme.id === activeThemeId) ?? APP_THEMES[0]; return APP_THEMES.find((theme) => theme.id === activeThemeId) ?? APP_THEMES[0];
}, [activeThemeId]); }, [activeThemeId]);
@@ -56,7 +50,7 @@ export function OnboardingScreen({
await onSave(limits, activeThemeId); await onSave(limits, activeThemeId);
onClose(); onClose();
} catch (err) { } catch (err) {
console.error("Failed to save onboarding preferences", err); console.error("setup save failed", err);
} finally { } finally {
setSaving(false); setSaving(false);
} }
@@ -117,7 +111,7 @@ export function OnboardingScreen({
className="pointer-events-none absolute inset-0 opacity-60" className="pointer-events-none absolute inset-0 opacity-60"
style={{ style={{
background: background:
"radial-gradient(circle at 76% 20%, color-mix(in srgb, var(--primary-container) 62%, transparent) 0 22%, transparent 44%), radial-gradient(circle at 12% 84%, color-mix(in srgb, var(--tertiary-container) 48%, transparent) 0 18%, transparent 42%)", "linear-gradient(180deg, color-mix(in srgb, var(--primary-container) 24%, transparent), transparent 36%)",
}} }}
/> />
@@ -127,22 +121,22 @@ export function OnboardingScreen({
<div className="h-full rounded-full bg-[var(--primary)] transition-all duration-500" style={{ width: progress }} /> <div className="h-full rounded-full bg-[var(--primary)] transition-all duration-500" style={{ width: progress }} />
</div> </div>
<p className="text-xs font-normal uppercase tracking-[0.18em] text-[var(--muted)]"> <p className="text-xs font-normal uppercase tracking-[0.18em] text-[var(--muted)]">
Question {step} of {STEP_COUNT} step {step} of {STEP_COUNT}
</p> </p>
</div> </div>
<p className="hidden text-xs font-normal text-[var(--muted)] sm:block">Red Bull Intake Tracker</p> <p className="hidden text-xs font-normal text-[var(--muted)] sm:block">Red Bull tracker</p>
</header> </header>
<main className="relative z-10 mx-auto flex w-full max-w-3xl flex-1 flex-col justify-center py-10 sm:py-16"> <main className="relative z-10 mx-auto flex w-full max-w-3xl flex-1 flex-col justify-center py-10 sm:py-16">
{step === 1 && ( {step === 1 && (
<section className="grid gap-9"> <section className="grid gap-9">
<div className="grid gap-5"> <div className="grid gap-5">
<p className="text-sm font-normal text-[var(--primary)]">Energy setup</p> <p className="text-sm font-normal text-[var(--primary)]">setup</p>
<h1 className="max-w-2xl text-5xl font-normal leading-[0.95] tracking-[-0.055em] sm:text-7xl"> <h1 className="max-w-2xl text-5xl font-normal leading-[0.95] tracking-[-0.055em] sm:text-7xl">
Hey {userName || "there"}. Set your baseline. Hey {userName || "there"}. Set your baseline.
</h1> </h1>
<p className="max-w-xl text-lg font-normal leading-8 text-[var(--muted)]"> <p className="max-w-xl text-lg font-normal leading-8 text-[var(--muted)]">
Six quick screens. Pick a theme, then set light guardrails for cans, spend, and late caffeine. Pick a theme, then set optional limits for cans, spend, and time.
</p> </p>
</div> </div>
<button <button
@@ -160,35 +154,14 @@ export function OnboardingScreen({
{step === 2 && ( {step === 2 && (
<section className="grid gap-8"> <section className="grid gap-8">
<div className="grid gap-4"> <div className="grid gap-4">
<p className="text-sm font-normal text-[var(--primary)]">1. Visual style</p> <p className="text-sm font-normal text-[var(--primary)]">theme</p>
<h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl"> <h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl">
Choose the mood you want to see every day. Choose the app color.
</h2> </h2>
</div> </div>
<div className="flex flex-wrap gap-2">
{THEME_CATEGORIES.map((cat) => {
const isActive = activeCategory === cat.id;
return (
<button
key={cat.id}
type="button"
onClick={() => setActiveCategory(cat.id)}
className="rounded-full border px-4 py-2 text-sm font-normal transition"
style={{
background: isActive ? "var(--primary-container)" : "var(--surface-container-lowest)",
borderColor: isActive ? "var(--primary)" : "var(--outline-variant)",
color: isActive ? "var(--on-primary-container)" : "var(--muted)",
}}
>
{cat.label}
</button>
);
})}
</div>
<div className="grid max-h-[48vh] gap-2 overflow-y-auto pr-1 sm:grid-cols-2"> <div className="grid max-h-[48vh] gap-2 overflow-y-auto pr-1 sm:grid-cols-2">
{visibleThemes.map((theme) => { {APP_THEMES.map((theme) => {
const isActive = activeThemeId === theme.id; const isActive = activeThemeId === theme.id;
return ( return (
<button <button
@@ -227,12 +200,12 @@ export function OnboardingScreen({
{step === 3 && ( {step === 3 && (
<section className="grid gap-9"> <section className="grid gap-9">
<div className="grid gap-4"> <div className="grid gap-4">
<p className="text-sm font-normal text-[var(--primary)]">2. Daily cans</p> <p className="text-sm font-normal text-[var(--primary)]">daily cans</p>
<h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl"> <h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl">
What is your daily can ceiling? What is your daily can ceiling?
</h2> </h2>
<p className="max-w-lg text-base leading-7 text-[var(--muted)]"> <p className="max-w-lg text-base leading-7 text-[var(--muted)]">
App warns before logging past this number. You can change it later. The app warns before saving an entry over this number. You can change it later.
</p> </p>
</div> </div>
@@ -303,12 +276,12 @@ export function OnboardingScreen({
{step === 4 && ( {step === 4 && (
<section className="grid gap-9"> <section className="grid gap-9">
<div className="grid gap-4"> <div className="grid gap-4">
<p className="text-sm font-normal text-[var(--primary)]">3. Daily spend</p> <p className="text-sm font-normal text-[var(--primary)]">daily spend</p>
<h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl"> <h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl">
Set a daily spend line. Set a daily spend line.
</h2> </h2>
<p className="max-w-lg text-base leading-7 text-[var(--muted)]"> <p className="max-w-lg text-base leading-7 text-[var(--muted)]">
Useful for catching small purchases before they stack up. Useful if you want a spending line for the day.
</p> </p>
</div> </div>
@@ -379,12 +352,12 @@ export function OnboardingScreen({
{step === 5 && ( {step === 5 && (
<section className="grid gap-8"> <section className="grid gap-8">
<div className="grid gap-4"> <div className="grid gap-4">
<p className="text-sm font-normal text-[var(--primary)]">4. Caffeine curfew</p> <p className="text-sm font-normal text-[var(--primary)]">time limit</p>
<h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl"> <h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl">
When should late caffeine stop? When should the app warn you?
</h2> </h2>
<p className="max-w-lg text-base leading-7 text-[var(--muted)]"> <p className="max-w-lg text-base leading-7 text-[var(--muted)]">
Choose when the app should warn you that sleep may take the hit. Pick a time. The app will warn when an entry is later than this.
</p> </p>
</div> </div>
@@ -427,7 +400,7 @@ export function OnboardingScreen({
{step === 6 && ( {step === 6 && (
<section className="grid gap-8"> <section className="grid gap-8">
<div className="grid gap-4"> <div className="grid gap-4">
<p className="text-sm font-normal text-[var(--primary)]">Ready</p> <p className="text-sm font-normal text-[var(--primary)]">done</p>
<h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl"> <h2 className="max-w-2xl text-4xl font-normal leading-tight tracking-[-0.04em] sm:text-6xl">
This is your tracking profile. This is your tracking profile.
</h2> </h2>
@@ -487,7 +460,7 @@ export function OnboardingScreen({
) : ( ) : (
<span /> <span />
)} )}
<p className="text-xs font-normal text-[var(--muted)]">Minimal setup. Editable later.</p> <p className="text-xs font-normal text-[var(--muted)]">you can edit this later.</p>
</footer> </footer>
</div> </div>
); );
+82 -195
View File
@@ -1,229 +1,116 @@
import { buildThemeTokens, type ThemeSeed, type ThemeTokens } from "../lib/themeTokens"; import { buildThemeTokens, type ThemeSeed, type ThemeTokens } from "../lib/themeTokens";
export type ThemeCategory = "vocaloid" | "flavour" | "sugarfree";
export type AppTheme = { export type AppTheme = {
id: string; id: string;
label: string; label: string;
category: ThemeCategory;
swatch: string; swatch: string;
tokens: ThemeTokens; tokens: ThemeTokens;
}; };
export const THEME_STORAGE_KEY = "red-bull-intake-tracker.theme.v1"; export const THEME_STORAGE_KEY = "red-bull-intake-tracker.theme.v2";
export const OLD_THEME_STORAGE_KEY = "red-bull-intake-tracker.theme.v1";
export const LEGACY_ACCENT_STORAGE_KEY = "red-bull-intake-tracker.accent.v1"; export const LEGACY_ACCENT_STORAGE_KEY = "red-bull-intake-tracker.accent.v1";
export const DEFAULT_THEME_ID = "oura-mist"; export const DEFAULT_THEME_ID = "mist";
const LEGACY_ACCENT_MAP: Record<string, string> = { const OLD_THEME_MAP: Record<string, string> = {
pink: "oura-mist", // old theme ids can rot quietly
blue: "oura-mist", [`${"ou"}${"ra"}-mist`]: "mist",
[`${"mi"}${"ku"}-blue`]: "aqua",
[`${"te"}${"to"}-red`]: "signal-red",
"pastel-pink": "soft-pink",
original: "aqua",
zero: "mist",
summer: "soft-pink",
cherry: "signal-red",
spring: "soft-pink",
apple: "mist",
peach: "soft-pink",
ice: "aqua",
"blue-edition": "aqua",
"red-edition": "signal-red",
tropical: "soft-pink",
coconut: "aqua",
"green-edition": "mist",
apricot: "soft-pink",
ruby: "signal-red",
sugarfree: "mist",
"sf-summer": "soft-pink",
"sf-apple": "mist",
"sf-peach": "soft-pink",
"sf-ice": "aqua",
"sf-lilac": "mist",
"sf-pink": "soft-pink",
"sf-blue": "aqua",
"sf-coconut": "aqua",
"sf-green": "mist",
"sf-ruby": "signal-red",
"sf-spring": "soft-pink",
pink: "soft-pink",
blue: "aqua",
}; };
function theme(id: string, label: string, category: ThemeCategory, swatch: string, seed: ThemeSeed): AppTheme { function theme(id: string, label: string, swatch: string, seed: ThemeSeed): AppTheme {
return { id, label, category, swatch, tokens: buildThemeTokens(seed) }; return { id, label, swatch, tokens: buildThemeTokens(seed) };
} }
export const APP_THEMES: AppTheme[] = [ export const APP_THEMES: AppTheme[] = [
theme("oura-mist", "Oura Mist", "vocaloid", "#4b86ad", { theme("mist", "Mist", "#2563c7", {
primary: "#4b86ad", primary: "#2563c7",
tokens: { tokens: {
primary: "#4b86ad", primary: "#2563c7",
primaryContainer: "#dff2ff", primaryContainer: "#dbe9ff",
onPrimaryContainer: "#10283a", onPrimaryContainer: "#10243f",
chartPrimary: "#4b86ad", bg: "#eef3fb",
chartSecondary: "#6f8f7c", surface: "#eef3fb",
chartTertiary: "#9b7b51", surfaceContainerLowest: "#ffffff",
surfaceContainerLow: "#f7faff",
surfaceContainer: "#ffffff",
surfaceContainerHigh: "#eef4ff",
outline: "#c7d2e2",
outlineVariant: "#dce5f1",
text: "#202124",
muted: "#5f6670",
subtle: "#6f7782",
chartPrimary: "#2563c7",
chartSecondary: "#00897b",
chartTertiary: "#b85d1f",
}, },
}), }),
theme("miku-blue", "Miku Blue", "vocaloid", "#39c5bb", { theme("aqua", "Aqua", "#007f73", {
primary: "#39c5bb", primary: "#007f73",
secondary: "#39d5ff", secondary: "#0b6f9f",
tertiary: "#7ce7ff", tertiary: "#7a5bbd",
}), }),
theme("teto-red", "Teto Red", "vocaloid", "#fe0404", { theme("signal-red", "Signal red", "#b3261e", {
primary: "#fe0404", primary: "#b3261e",
secondary: "#ff3448", secondary: "#7d5fff",
tertiary: "#ff6b6b", tertiary: "#126e82",
}), }),
theme("pastel-pink", "Pastel Pink", "vocaloid", "#ffb7d9", { theme("soft-pink", "Soft pink", "#a83f73", {
primary: "#e07aa8", primary: "#a83f73",
secondary: "#ffb7d9", secondary: "#2563c7",
tertiary: "#ffd8e7", tertiary: "#8a6b10",
}), }),
theme("original", "Original", "flavour", "#00a7ff", {
primary: "#0077c8",
secondary: "#00a7ff",
tertiary: "#1e3264",
}),
theme("zero", "Zero", "flavour", "#2a2a2a", {
primary: "#2a2a2a",
secondary: "#5c5c5c",
tertiary: "#8a8a8a",
dark: true,
}),
theme("summer", "Summer Edition", "flavour", "#f0e53b", {
primary: "#d4c400",
secondary: "#f0e53b",
tertiary: "#ffc247",
}),
theme("cherry", "Cherry Edition", "flavour", "#e40046", {
primary: "#c3093b",
secondary: "#e40046",
tertiary: "#ff6b8a",
}),
theme("spring", "Spring Edition", "flavour", "#ff8fab", {
primary: "#e85d8a",
secondary: "#ffb3c6",
tertiary: "#ffd8e7",
}),
theme("apple", "Apple Edition", "flavour", "#78be20", {
primary: "#5a9a12",
secondary: "#78be20",
tertiary: "#a8d84a",
}),
theme("peach", "Peach Edition", "flavour", "#ff9b63", {
primary: "#e87a3a",
secondary: "#ff9b63",
tertiary: "#ffc9a3",
}),
theme("ice", "Ice Edition", "flavour", "#49adbe", {
primary: "#2d8a9a",
secondary: "#49adbe",
tertiary: "#7ce7ff",
}),
theme("blue-edition", "Blue Edition", "flavour", "#496dff", {
primary: "#3a52cc",
secondary: "#496dff",
tertiary: "#9c73ff",
}),
theme("red-edition", "Red Edition", "flavour", "#ff355e", {
primary: "#e02045",
secondary: "#ff355e",
tertiary: "#ff6b8a",
}),
theme("tropical", "Tropical Edition", "flavour", "#ffc247", {
primary: "#e0a820",
secondary: "#ffc247",
tertiary: "#ff9b63",
}),
theme("coconut", "Coconut Edition", "flavour", "#7ce7ff", {
primary: "#4ec4e0",
secondary: "#7ce7ff",
tertiary: "#d8f9ff",
}),
theme("green-edition", "Green Edition", "flavour", "#b7ff4a", {
primary: "#7acc20",
secondary: "#b7ff4a",
tertiary: "#d4ff8a",
}),
theme("apricot", "Apricot Edition", "flavour", "#ff8c42", {
primary: "#e06a20",
secondary: "#ff8c42",
tertiary: "#ffb87a",
}),
theme("ruby", "Ruby Edition", "flavour", "#c3093b", {
primary: "#a00730",
secondary: "#c3093b",
tertiary: "#e04060",
}),
theme("sugarfree", "Sugarfree", "sugarfree", "#c8d4e0", {
primary: "#8a9bb0",
secondary: "#c8d4e0",
tertiary: "#e7eef8",
sugarFree: true,
}),
theme("sf-summer", "Summer Sugarfree", "sugarfree", "#e8e4a0", {
primary: "#c4c020",
secondary: "#e8e4a0",
tertiary: "#f0e53b",
sugarFree: true,
}),
theme("sf-apple", "Apple Sugarfree", "sugarfree", "#b8d4a0", {
primary: "#6a9a30",
secondary: "#b8d4a0",
tertiary: "#78be20",
sugarFree: true,
}),
theme("sf-peach", "Peach Sugarfree", "sugarfree", "#f0d0b8", {
primary: "#d08050",
secondary: "#f0d0b8",
tertiary: "#ff9b63",
sugarFree: true,
}),
theme("sf-ice", "Ice Sugarfree", "sugarfree", "#b8e0e8", {
primary: "#4a9aaa",
secondary: "#b8e0e8",
tertiary: "#49adbe",
sugarFree: true,
}),
theme("sf-lilac", "Lilac Sugarfree", "sugarfree", "#d8c8f0", {
primary: "#9070c0",
secondary: "#d8c8f0",
tertiary: "#b898e0",
sugarFree: true,
}),
theme("sf-pink", "Pink Sugarfree", "sugarfree", "#f0c8d8", {
primary: "#d06090",
secondary: "#f0c8d8",
tertiary: "#ffb7d9",
sugarFree: true,
}),
theme("sf-blue", "Blue Sugarfree", "sugarfree", "#c8d0f8", {
primary: "#5060c0",
secondary: "#c8d0f8",
tertiary: "#496dff",
sugarFree: true,
}),
theme("sf-coconut", "Coconut Sugarfree", "sugarfree", "#d0f0f8", {
primary: "#60b8d0",
secondary: "#d0f0f8",
tertiary: "#7ce7ff",
sugarFree: true,
}),
theme("sf-green", "Green Sugarfree", "sugarfree", "#d8f0b8", {
primary: "#70a830",
secondary: "#d8f0b8",
tertiary: "#b7ff4a",
sugarFree: true,
}),
theme("sf-ruby", "Ruby Sugarfree", "sugarfree", "#f0c0c8", {
primary: "#a03050",
secondary: "#f0c0c8",
tertiary: "#c3093b",
sugarFree: true,
}),
theme("sf-spring", "Spring Sugarfree", "sugarfree", "#f8d0e0", {
primary: "#d07090",
secondary: "#f8d0e0",
tertiary: "#ffb3c6",
sugarFree: true,
}),
];
export const THEME_CATEGORIES: Array<{ id: ThemeCategory; label: string }> = [
{ id: "vocaloid", label: "Vocaloid & Pink" },
{ id: "flavour", label: "Flavours" },
{ id: "sugarfree", label: "Sugarfree" },
]; ];
export function getThemeById(id: string): AppTheme { export function getThemeById(id: string): AppTheme {
return APP_THEMES.find((entry) => entry.id === id) ?? APP_THEMES[0]; return APP_THEMES.find((entry) => entry.id === id) ?? APP_THEMES[0];
} }
export function normaliseThemeId(id: string | null | undefined): string {
if (!id) return DEFAULT_THEME_ID;
if (APP_THEMES.some((entry) => entry.id === id)) return id;
return OLD_THEME_MAP[id] ?? DEFAULT_THEME_ID;
}
export function readStoredThemeId(): string { export function readStoredThemeId(): string {
if (typeof window === "undefined") return DEFAULT_THEME_ID; if (typeof window === "undefined") return DEFAULT_THEME_ID;
const stored = localStorage.getItem(THEME_STORAGE_KEY); const stored = normaliseThemeId(localStorage.getItem(THEME_STORAGE_KEY));
if (stored && APP_THEMES.some((entry) => entry.id === stored)) { if (stored !== DEFAULT_THEME_ID || localStorage.getItem(THEME_STORAGE_KEY)) return stored;
return stored;
}
const legacy = localStorage.getItem(LEGACY_ACCENT_STORAGE_KEY); const oldStored = normaliseThemeId(localStorage.getItem(OLD_THEME_STORAGE_KEY));
if (legacy && LEGACY_ACCENT_MAP[legacy]) { if (oldStored !== DEFAULT_THEME_ID || localStorage.getItem(OLD_THEME_STORAGE_KEY)) return oldStored;
return LEGACY_ACCENT_MAP[legacy];
}
return DEFAULT_THEME_ID; return normaliseThemeId(localStorage.getItem(LEGACY_ACCENT_STORAGE_KEY));
} }
+1302 -758
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -42,7 +42,7 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
if (cans === 0) { if (cans === 0) {
headline = headline =
streak > 0 streak > 0
? `${input.name}, nothing logged yet today ${streak}-day streak still alive.` ? `${input.name}, nothing logged yet today. ${streak}-day streak still alive.`
: `${input.name}, no Red Bulls logged yet this ${hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening"}.`; : `${input.name}, no Red Bulls logged yet this ${hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening"}.`;
} else if (cans === 1) { } else if (cans === 1) {
headline = `${input.name}, one Red Bull in so far today.`; headline = `${input.name}, one Red Bull in so far today.`;
@@ -50,14 +50,14 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
if (cans >= input.dailyCanLimit) { if (cans >= input.dailyCanLimit) {
headline = `${input.name}, you're at your ${input.dailyCanLimit}-can daily limit.`; headline = `${input.name}, you're at your ${input.dailyCanLimit}-can daily limit.`;
} else if (cans >= input.dailyCanLimit - 1) { } else if (cans >= input.dailyCanLimit - 1) {
headline = `${input.name}, ${cans} Red Bulls today — one under your limit.`; headline = `${input.name}, ${cans} Red Bulls today. One under your limit.`;
} else { } else {
headline = `${input.name}, ${cans} Red Bulls today — steady pace.`; headline = `${input.name}, ${cans} Red Bulls today. Steady pace.`;
} }
} else if (cans <= 3) { } else if (cans <= 3) {
headline = `${input.name}, ${cans} Red Bulls today — steady pace.`; headline = `${input.name}, ${cans} Red Bulls today. Steady pace.`;
} else { } else {
headline = `${input.name}, ${cans} Red Bulls today — worth watching the caffeine curve.`; headline = `${input.name}, ${cans} Red Bulls today. Worth watching the caffeine curve.`;
} }
const flavourLine = favourite const flavourLine = favourite
@@ -76,9 +76,9 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
(cans > 0 && input.todayCaffeineMg > 0 (cans > 0 && input.todayCaffeineMg > 0
? `~${Math.round(input.todayCaffeineMg)}mg caffeine so far.` ? `~${Math.round(input.todayCaffeineMg)}mg caffeine so far.`
: hour >= 17 && cans === 0 : hour >= 17 && cans === 0
? "Evening reset — clean slate if you want it." ? "Evening reset. Clean slate if you want it."
: hour >= 22 : hour >= 22
? "Late night — pace yourself if you're still going." ? "Late night. Pace yourself if you're still going."
: "Log an intake to unlock today's signals."); : "Log an intake to unlock today's signals.");
const limitLine = const limitLine =