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:
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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
|
||||
name="description"
|
||||
content="A local-first Red Bull intake web app for tracking cans, spending, caffeine, sugar, flavours, and trends."
|
||||
|
||||
@@ -15,8 +15,8 @@ export function DailyLimitsCard({ limits, check, onOpenSettings }: DailyLimitsCa
|
||||
<section className="limits-card glass-panel p-5 sm:p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium uppercase tracking-[0.18em] text-cyan-100">Daily limits</p>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-400">
|
||||
<p className="section-kicker">Daily limits</p>
|
||||
<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
|
||||
account.
|
||||
</p>
|
||||
@@ -37,7 +37,7 @@ export function DailyLimitsCard({ limits, check, onOpenSettings }: DailyLimitsCa
|
||||
return (
|
||||
<section className="limits-card glass-panel p-5 sm:p-6">
|
||||
<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}>
|
||||
<Settings2 size={14} aria-hidden="true" />
|
||||
Edit
|
||||
|
||||
@@ -56,7 +56,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
|
||||
<form className="grid gap-4" onSubmit={submit}>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<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
|
||||
className="field-input"
|
||||
type="number"
|
||||
@@ -70,7 +70,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
|
||||
</label>
|
||||
|
||||
<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
|
||||
className="field-input"
|
||||
type="number"
|
||||
@@ -85,7 +85,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
|
||||
</div>
|
||||
|
||||
<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
|
||||
className="field-input"
|
||||
type="time"
|
||||
@@ -96,7 +96,7 @@ export function LimitsSettingsForm({ limits, check, saving, onSave }: LimitsSett
|
||||
</label>
|
||||
|
||||
{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(" · ")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "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 type { UserLimits } from "../types";
|
||||
|
||||
@@ -33,12 +33,6 @@ export function OnboardingScreen({
|
||||
const [dailySpendLimit, setDailySpendLimit] = useState<number | "none">(3.5);
|
||||
const [stopTime, setStopTime] = useState<string | "none">("18:00");
|
||||
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(() => {
|
||||
return APP_THEMES.find((theme) => theme.id === activeThemeId) ?? APP_THEMES[0];
|
||||
}, [activeThemeId]);
|
||||
@@ -56,7 +50,7 @@ export function OnboardingScreen({
|
||||
await onSave(limits, activeThemeId);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error("Failed to save onboarding preferences", err);
|
||||
console.error("setup save failed", err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -117,7 +111,7 @@ export function OnboardingScreen({
|
||||
className="pointer-events-none absolute inset-0 opacity-60"
|
||||
style={{
|
||||
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>
|
||||
<p className="text-xs font-normal uppercase tracking-[0.18em] text-[var(--muted)]">
|
||||
Question {step} of {STEP_COUNT}
|
||||
step {step} of {STEP_COUNT}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<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 && (
|
||||
<section className="grid gap-9">
|
||||
<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">
|
||||
Hey {userName || "there"}. Set your baseline.
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
@@ -160,35 +154,14 @@ export function OnboardingScreen({
|
||||
{step === 2 && (
|
||||
<section className="grid gap-8">
|
||||
<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">
|
||||
Choose the mood you want to see every day.
|
||||
Choose the app color.
|
||||
</h2>
|
||||
</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">
|
||||
{visibleThemes.map((theme) => {
|
||||
{APP_THEMES.map((theme) => {
|
||||
const isActive = activeThemeId === theme.id;
|
||||
return (
|
||||
<button
|
||||
@@ -227,12 +200,12 @@ export function OnboardingScreen({
|
||||
{step === 3 && (
|
||||
<section className="grid gap-9">
|
||||
<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">
|
||||
What is your daily can ceiling?
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -303,12 +276,12 @@ export function OnboardingScreen({
|
||||
{step === 4 && (
|
||||
<section className="grid gap-9">
|
||||
<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">
|
||||
Set a daily spend line.
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -379,12 +352,12 @@ export function OnboardingScreen({
|
||||
{step === 5 && (
|
||||
<section className="grid gap-8">
|
||||
<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">
|
||||
When should late caffeine stop?
|
||||
When should the app warn you?
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -427,7 +400,7 @@ export function OnboardingScreen({
|
||||
{step === 6 && (
|
||||
<section className="grid gap-8">
|
||||
<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">
|
||||
This is your tracking profile.
|
||||
</h2>
|
||||
@@ -487,7 +460,7 @@ export function OnboardingScreen({
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
+82
-195
@@ -1,229 +1,116 @@
|
||||
import { buildThemeTokens, type ThemeSeed, type ThemeTokens } from "../lib/themeTokens";
|
||||
|
||||
export type ThemeCategory = "vocaloid" | "flavour" | "sugarfree";
|
||||
|
||||
export type AppTheme = {
|
||||
id: string;
|
||||
label: string;
|
||||
category: ThemeCategory;
|
||||
swatch: string;
|
||||
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 DEFAULT_THEME_ID = "oura-mist";
|
||||
export const DEFAULT_THEME_ID = "mist";
|
||||
|
||||
const LEGACY_ACCENT_MAP: Record<string, string> = {
|
||||
pink: "oura-mist",
|
||||
blue: "oura-mist",
|
||||
const OLD_THEME_MAP: Record<string, string> = {
|
||||
// old theme ids can rot quietly
|
||||
[`${"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 {
|
||||
return { id, label, category, swatch, tokens: buildThemeTokens(seed) };
|
||||
function theme(id: string, label: string, swatch: string, seed: ThemeSeed): AppTheme {
|
||||
return { id, label, swatch, tokens: buildThemeTokens(seed) };
|
||||
}
|
||||
|
||||
export const APP_THEMES: AppTheme[] = [
|
||||
theme("oura-mist", "Oura Mist", "vocaloid", "#4b86ad", {
|
||||
primary: "#4b86ad",
|
||||
theme("mist", "Mist", "#2563c7", {
|
||||
primary: "#2563c7",
|
||||
tokens: {
|
||||
primary: "#4b86ad",
|
||||
primaryContainer: "#dff2ff",
|
||||
onPrimaryContainer: "#10283a",
|
||||
chartPrimary: "#4b86ad",
|
||||
chartSecondary: "#6f8f7c",
|
||||
chartTertiary: "#9b7b51",
|
||||
primary: "#2563c7",
|
||||
primaryContainer: "#dbe9ff",
|
||||
onPrimaryContainer: "#10243f",
|
||||
bg: "#eef3fb",
|
||||
surface: "#eef3fb",
|
||||
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", {
|
||||
primary: "#39c5bb",
|
||||
secondary: "#39d5ff",
|
||||
tertiary: "#7ce7ff",
|
||||
theme("aqua", "Aqua", "#007f73", {
|
||||
primary: "#007f73",
|
||||
secondary: "#0b6f9f",
|
||||
tertiary: "#7a5bbd",
|
||||
}),
|
||||
theme("teto-red", "Teto Red", "vocaloid", "#fe0404", {
|
||||
primary: "#fe0404",
|
||||
secondary: "#ff3448",
|
||||
tertiary: "#ff6b6b",
|
||||
theme("signal-red", "Signal red", "#b3261e", {
|
||||
primary: "#b3261e",
|
||||
secondary: "#7d5fff",
|
||||
tertiary: "#126e82",
|
||||
}),
|
||||
theme("pastel-pink", "Pastel Pink", "vocaloid", "#ffb7d9", {
|
||||
primary: "#e07aa8",
|
||||
secondary: "#ffb7d9",
|
||||
tertiary: "#ffd8e7",
|
||||
theme("soft-pink", "Soft pink", "#a83f73", {
|
||||
primary: "#a83f73",
|
||||
secondary: "#2563c7",
|
||||
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 {
|
||||
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 {
|
||||
if (typeof window === "undefined") return DEFAULT_THEME_ID;
|
||||
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored && APP_THEMES.some((entry) => entry.id === stored)) {
|
||||
return stored;
|
||||
}
|
||||
const stored = normaliseThemeId(localStorage.getItem(THEME_STORAGE_KEY));
|
||||
if (stored !== DEFAULT_THEME_ID || localStorage.getItem(THEME_STORAGE_KEY)) return stored;
|
||||
|
||||
const legacy = localStorage.getItem(LEGACY_ACCENT_STORAGE_KEY);
|
||||
if (legacy && LEGACY_ACCENT_MAP[legacy]) {
|
||||
return LEGACY_ACCENT_MAP[legacy];
|
||||
}
|
||||
const oldStored = normaliseThemeId(localStorage.getItem(OLD_THEME_STORAGE_KEY));
|
||||
if (oldStored !== DEFAULT_THEME_ID || localStorage.getItem(OLD_THEME_STORAGE_KEY)) return oldStored;
|
||||
|
||||
return DEFAULT_THEME_ID;
|
||||
return normaliseThemeId(localStorage.getItem(LEGACY_ACCENT_STORAGE_KEY));
|
||||
}
|
||||
|
||||
+1302
-758
File diff suppressed because it is too large
Load Diff
+7
-7
@@ -42,7 +42,7 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
|
||||
if (cans === 0) {
|
||||
headline =
|
||||
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"}.`;
|
||||
} else if (cans === 1) {
|
||||
headline = `${input.name}, one Red Bull in so far today.`;
|
||||
@@ -50,14 +50,14 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
|
||||
if (cans >= input.dailyCanLimit) {
|
||||
headline = `${input.name}, you're at your ${input.dailyCanLimit}-can daily limit.`;
|
||||
} 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 {
|
||||
headline = `${input.name}, ${cans} Red Bulls today — steady pace.`;
|
||||
headline = `${input.name}, ${cans} Red Bulls today. Steady pace.`;
|
||||
}
|
||||
} else if (cans <= 3) {
|
||||
headline = `${input.name}, ${cans} Red Bulls today — steady pace.`;
|
||||
headline = `${input.name}, ${cans} Red Bulls today. Steady pace.`;
|
||||
} 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
|
||||
@@ -76,9 +76,9 @@ export function buildDynamicGreeting(input: GreetingInput): GreetingResult {
|
||||
(cans > 0 && input.todayCaffeineMg > 0
|
||||
? `~${Math.round(input.todayCaffeineMg)}mg caffeine so far.`
|
||||
: hour >= 17 && cans === 0
|
||||
? "Evening reset — clean slate if you want it."
|
||||
? "Evening reset. Clean slate if you want it."
|
||||
: 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.");
|
||||
|
||||
const limitLine =
|
||||
|
||||
Reference in New Issue
Block a user