feat: update typography and color scheme; integrate Google Sans fonts
- Replaced "SF Pro Text" and related fonts with "Google Sans", "Google Sans Text", and "Product Sans" in CSS and Tailwind configuration. - Adjusted color variables for primary, secondary, and error states to enhance UI consistency. - Modified background colors and button styles for improved aesthetics and usability. - Introduced new utility classes for layout and component styling in the CSS.
This commit is contained in:
+106
-82
@@ -132,10 +132,17 @@ const NAV_ITEMS: Array<{ id: AppView; label: string; icon: LucideIcon }> = [
|
||||
];
|
||||
|
||||
const ACCENT_OPTIONS: Array<{ id: AccentTheme; label: string }> = [
|
||||
{ id: "blue", label: "Baby blue" },
|
||||
{ id: "pink", label: "Pastel pink" },
|
||||
];
|
||||
|
||||
const MATERIAL_ACCENTS = {
|
||||
primary: "var(--chart-primary)",
|
||||
secondary: "var(--chart-secondary)",
|
||||
tertiary: "var(--chart-tertiary)",
|
||||
error: "var(--chart-error)",
|
||||
custom: "#b85d84",
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [themeAccent, setThemeAccent] = useState<AccentTheme>(() => readStoredAccent());
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
@@ -522,7 +529,7 @@ function App() {
|
||||
|
||||
<ShellBackdrop />
|
||||
|
||||
<div className="mx-auto grid w-full max-w-[1680px] gap-4 px-3 py-3 lg:grid-cols-[280px_1fr] lg:px-5 lg:py-5">
|
||||
<div className="app-layout">
|
||||
<Sidebar
|
||||
accent={themeAccent}
|
||||
activeView={activeView}
|
||||
@@ -531,11 +538,12 @@ function App() {
|
||||
setupStatus={setupStatus}
|
||||
user={user}
|
||||
onAccentChange={setThemeAccent}
|
||||
onAdd={openNewEntry}
|
||||
onChange={setActiveView}
|
||||
onLogout={() => void logout()}
|
||||
/>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="app-content">
|
||||
<MobileNav activeView={activeView} onChange={setActiveView} />
|
||||
|
||||
<TopBar
|
||||
@@ -561,7 +569,7 @@ function App() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="mt-4"
|
||||
className="app-main"
|
||||
>
|
||||
{activeView === "overview" && (
|
||||
<OverviewView
|
||||
@@ -675,7 +683,7 @@ function LoadingScreen({ setupStatus, themeAccent }: { setupStatus: SetupStatus;
|
||||
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-lg border border-cyan-300/40 bg-cyan-300/10 text-cyan-200">
|
||||
<Loader2 className="animate-spin" size={24} aria-hidden="true" />
|
||||
</div>
|
||||
<h1 className="mt-5 text-2xl font-semibold tracking-tight text-white">Red Bull command centre</h1>
|
||||
<h1 className="mt-5 text-2xl font-semibold tracking-tight text-white">Red Bull tracker</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">{setupStatus.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -719,9 +727,9 @@ function AuthView({
|
||||
return (
|
||||
<div className="app-shell min-h-screen bg-[#050711] text-slate-100" data-accent={accent}>
|
||||
<ShellBackdrop />
|
||||
<main className="mx-auto grid min-h-screen w-full max-w-6xl items-center gap-6 px-4 py-8 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<section className="min-w-0">
|
||||
<div className="mb-4 inline-flex items-center gap-2 rounded-md border border-cyan-300/30 bg-cyan-300/10 px-3 py-2 text-sm font-semibold text-cyan-100">
|
||||
<main className="auth-layout">
|
||||
<section className="auth-hero">
|
||||
<div className="state-chip mb-4">
|
||||
<Cloud size={16} aria-hidden="true" />
|
||||
{setupStatus.state === "ok" ? "Appwrite sync online" : "Appwrite setup check"}
|
||||
</div>
|
||||
@@ -729,9 +737,9 @@ function AuthView({
|
||||
Red Bull Tracker App
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-base leading-7 text-slate-300">
|
||||
Glossy intake telemetry with Appwrite authentication, device sync, and finance-grade Excel exports.
|
||||
Soft Material You intake tracking with Appwrite authentication, device sync, and polished Excel exports.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-3 sm:grid-cols-3">
|
||||
<div className="auth-signal-grid">
|
||||
<AuthSignal icon={ShieldCheck} label="User scoped" value="Private entries" />
|
||||
<AuthSignal icon={Database} label="Database" value={appwriteConfig.databaseId} />
|
||||
<AuthSignal icon={CheckCircle2} label="Ping" value={setupStatus.state === "ok" ? "Connected" : "Check setup"} />
|
||||
@@ -746,17 +754,17 @@ function AuthView({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="glass-panel p-5 sm:p-6">
|
||||
<div className="mb-5 flex rounded-md border border-white/10 bg-white/5 p-1">
|
||||
<section className="auth-panel">
|
||||
<div className="segmented-control mb-5">
|
||||
<button
|
||||
className={`flex-1 rounded px-3 py-2 text-sm font-semibold transition ${mode === "login" ? "bg-cyan-300 text-[#07101f]" : "text-slate-300 hover:bg-white/10"}`}
|
||||
className={mode === "login" ? "segmented-control-active" : ""}
|
||||
type="button"
|
||||
onClick={() => setMode("login")}
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 rounded px-3 py-2 text-sm font-semibold transition ${mode === "signup" ? "bg-pink-200 text-[#07101f]" : "text-slate-300 hover:bg-white/10"}`}
|
||||
className={mode === "signup" ? "segmented-control-active" : ""}
|
||||
type="button"
|
||||
onClick={() => setMode("signup")}
|
||||
>
|
||||
@@ -856,6 +864,7 @@ function Sidebar({
|
||||
setupStatus,
|
||||
user,
|
||||
onAccentChange,
|
||||
onAdd,
|
||||
onChange,
|
||||
onLogout,
|
||||
}: {
|
||||
@@ -866,22 +875,28 @@ function Sidebar({
|
||||
setupStatus: SetupStatus;
|
||||
user: AuthUser;
|
||||
onAccentChange: (accent: AccentTheme) => void;
|
||||
onAdd: () => void;
|
||||
onChange: (view: AppView) => void;
|
||||
onLogout: () => void;
|
||||
}) {
|
||||
return (
|
||||
<aside className="glass-panel sticky top-5 hidden h-[calc(100vh-2.5rem)] p-3 lg:flex lg:flex-col">
|
||||
<div className="mb-7 flex items-center gap-3 px-2 pt-1">
|
||||
<aside className="material-drawer">
|
||||
<div className="drawer-brand">
|
||||
<div className="can-emblem">
|
||||
<Command size={22} aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-white">Red Bull</p>
|
||||
<p className="truncate text-xs text-cyan-100">Intake telemetry</p>
|
||||
<p className="truncate text-xs text-cyan-100">Intake tracker</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="grid gap-1" aria-label="Main navigation">
|
||||
<button className="drawer-primary-action" type="button" onClick={onAdd}>
|
||||
<Plus size={19} aria-hidden="true" />
|
||||
Add intake
|
||||
</button>
|
||||
|
||||
<nav className="drawer-nav" aria-label="Main navigation">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
@@ -899,8 +914,8 @@ function Sidebar({
|
||||
<AccentPicker accent={accent} onChange={onAccentChange} />
|
||||
</div>
|
||||
|
||||
<div className="mt-auto grid gap-3">
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.06] p-3">
|
||||
<div className="drawer-footer">
|
||||
<div className="drawer-info-card">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-400">
|
||||
{dataLoading ? <Loader2 className="animate-spin text-cyan-200" size={15} aria-hidden="true" /> : <Cloud className="text-cyan-200" size={15} aria-hidden="true" />}
|
||||
Sync
|
||||
@@ -909,7 +924,7 @@ function Sidebar({
|
||||
<p className={`mt-2 text-xs ${setupStatus.state === "ok" ? "text-emerald-200" : "text-amber-200"}`}>{setupStatus.message}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-white/[0.06] p-3">
|
||||
<div className="drawer-info-card">
|
||||
<p className="truncate text-sm font-semibold text-white">{user.name || user.email || "Appwrite user"}</p>
|
||||
<p className="mt-1 truncate text-xs text-slate-400">{user.email}</p>
|
||||
<button className="secondary-button mt-3 w-full justify-center" type="button" onClick={onLogout}>
|
||||
@@ -924,14 +939,12 @@ function Sidebar({
|
||||
|
||||
function MobileNav({ activeView, onChange }: { activeView: AppView; onChange: (view: AppView) => void }) {
|
||||
return (
|
||||
<nav className="sticky top-3 z-30 mb-3 grid grid-cols-4 gap-1 rounded-lg border border-white/10 bg-[#090f22]/90 p-1 shadow-fridge backdrop-blur-xl lg:hidden" aria-label="Main navigation">
|
||||
<nav className="mobile-nav-bar lg:hidden" aria-label="Main navigation">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`flex min-h-11 flex-col items-center justify-center gap-1 rounded-md text-[11px] font-medium transition ${
|
||||
activeView === item.id ? "bg-cyan-300 text-[#07101f] shadow-cyan" : "text-slate-300 hover:bg-white/10"
|
||||
}`}
|
||||
className={`mobile-nav-item ${activeView === item.id ? "mobile-nav-item-active" : ""}`}
|
||||
onClick={() => onChange(item.id)}
|
||||
>
|
||||
<item.icon size={16} aria-hidden="true" />
|
||||
@@ -967,7 +980,9 @@ function TopBar({
|
||||
onImportExcel: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const title = NAV_ITEMS.find((item) => item.id === activeView)?.label ?? "Overview";
|
||||
const activeItem = NAV_ITEMS.find((item) => item.id === activeView) ?? NAV_ITEMS[0];
|
||||
const title = activeItem.label;
|
||||
const ActiveIcon = activeItem.icon;
|
||||
const subtitle = new Intl.DateTimeFormat("en-GB", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
@@ -975,22 +990,32 @@ function TopBar({
|
||||
}).format(new Date());
|
||||
|
||||
return (
|
||||
<header className="glass-panel p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="flex flex-wrap items-center gap-2 text-sm font-medium text-cyan-100">
|
||||
<span>{subtitle}</span>
|
||||
<span className="rounded bg-white/10 px-2 py-1 text-xs text-slate-300">{user.email || "Synced user"}</span>
|
||||
</p>
|
||||
<h1 className="mt-1 text-4xl font-semibold tracking-tight text-white sm:text-5xl">{title}</h1>
|
||||
<header className="top-app-bar">
|
||||
<div className="top-app-bar-main">
|
||||
<div className="top-title-cluster">
|
||||
<span className="top-app-icon">
|
||||
<ActiveIcon size={24} aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="top-kicker">{subtitle}</p>
|
||||
<h1 className="top-title">{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="top-meta-row">
|
||||
<span className="account-chip">{user.email || "Synced user"}</span>
|
||||
<AccentPicker accent={accent} onChange={onAccentChange} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-action-row">
|
||||
<div className="top-action-primary">
|
||||
<button className="primary-button" type="button" onClick={onAdd} disabled={Boolean(actionLoading)}>
|
||||
<Plus size={18} aria-hidden="true" />
|
||||
Add Intake
|
||||
</button>
|
||||
</div>
|
||||
<div className="top-action-secondary">
|
||||
<button className="secondary-button" type="button" onClick={onRefresh} disabled={dataLoading}>
|
||||
{dataLoading ? <Loader2 className="animate-spin" size={17} aria-hidden="true" /> : <RefreshCcw size={17} aria-hidden="true" />}
|
||||
Sync
|
||||
@@ -1074,28 +1099,28 @@ function OverviewView({
|
||||
</section>
|
||||
|
||||
<section className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricTile icon={CalendarDays} label="This Month" value={dashboard.monthCans} detail={`${dashboard.monthSpend} spent`} accent="#39d5ff" />
|
||||
<MetricTile icon={PoundSterling} label="Total Spend" value={dashboard.totalSpend} detail={`${dashboard.avgWeeklySpend} weekly average`} accent="#ffb7d9" />
|
||||
<MetricTile icon={Activity} label="Favourite" value={dashboard.favouriteFlavour} detail="by total cans" accent="#ffd84d" />
|
||||
<MetricTile icon={TimerReset} label="Days Without" value={dashboard.daysWithoutRedBull} detail={`${dashboard.currentStreak} day streak`} accent="#ff3448" />
|
||||
<MetricTile icon={CalendarDays} label="This Month" value={dashboard.monthCans} detail={`${dashboard.monthSpend} spent`} accent={MATERIAL_ACCENTS.primary} />
|
||||
<MetricTile icon={PoundSterling} label="Total Spend" value={dashboard.totalSpend} detail={`${dashboard.avgWeeklySpend} weekly average`} accent={MATERIAL_ACCENTS.secondary} />
|
||||
<MetricTile icon={Activity} label="Favourite" value={dashboard.favouriteFlavour} detail="by total cans" accent={MATERIAL_ACCENTS.tertiary} />
|
||||
<MetricTile icon={TimerReset} label="Days Without" value={dashboard.daysWithoutRedBull} detail={`${dashboard.currentStreak} day streak`} accent={MATERIAL_ACCENTS.error} />
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[1.25fr_0.75fr]">
|
||||
<AppCard title="Spend telemetry" subtitle="Last 30 logged days">
|
||||
<AppCard title="Spend overview" subtitle="Last 30 logged days">
|
||||
{chartData.length ? (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<AreaChart data={chartData} margin={{ top: 12, right: 12, bottom: 0, left: -18 }}>
|
||||
<defs>
|
||||
<linearGradient id="mikuSpend" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#39d5ff" stopOpacity={0.36} />
|
||||
<stop offset="100%" stopColor="#39d5ff" stopOpacity={0.03} />
|
||||
<linearGradient id="spendGradient" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor={MATERIAL_ACCENTS.primary} stopOpacity={0.28} />
|
||||
<stop offset="100%" stopColor={MATERIAL_ACCENTS.primary} stopOpacity={0.03} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke="#39d5ff" fill="url(#mikuSpend)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke={MATERIAL_ACCENTS.primary} fill="url(#spendGradient)" strokeWidth={3} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
@@ -1131,7 +1156,7 @@ function OverviewView({
|
||||
{flavourData.length ? (
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<PieChart>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={70} outerRadius={104} paddingAngle={4} stroke="#080d1f" strokeWidth={4}>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={70} outerRadius={104} paddingAngle={4} stroke="var(--surface-container)" strokeWidth={4}>
|
||||
{flavourData.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.accent} />
|
||||
))}
|
||||
@@ -1169,12 +1194,12 @@ function TodayPanel({
|
||||
<p className="mt-2 text-lg text-slate-300">cans logged</p>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-3 lg:min-w-[420px]">
|
||||
<MiniMetric label="Caffeine" value={dashboard.todayCaffeine} accent="#39d5ff" />
|
||||
<MiniMetric label="Sugar" value={dashboard.todaySugar} accent="#ffb7d9" />
|
||||
<MiniMetric label="Streak" value={dashboard.currentStreak} accent="#ffd84d" />
|
||||
<MiniMetric label="Caffeine" value={dashboard.todayCaffeine} accent={MATERIAL_ACCENTS.primary} />
|
||||
<MiniMetric label="Sugar" value={dashboard.todaySugar} accent={MATERIAL_ACCENTS.secondary} />
|
||||
<MiniMetric label="Streak" value={dashboard.currentStreak} accent={MATERIAL_ACCENTS.tertiary} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-wrap items-center gap-2">
|
||||
<div className="today-action-row mt-6 hidden flex-wrap items-center gap-2 lg:flex">
|
||||
<button className="primary-button" type="button" onClick={onAdd}>
|
||||
<Plus size={18} aria-hidden="true" />
|
||||
Add intake
|
||||
@@ -1276,20 +1301,20 @@ function TrendsView({
|
||||
<AreaChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<defs>
|
||||
<linearGradient id="trendSpend" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#39d5ff" stopOpacity={0.28} />
|
||||
<stop offset="100%" stopColor="#39d5ff" stopOpacity={0.02} />
|
||||
<stop offset="0%" stopColor={MATERIAL_ACCENTS.primary} stopOpacity={0.26} />
|
||||
<stop offset="100%" stopColor={MATERIAL_ACCENTS.primary} stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
<linearGradient id="trendCans" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stopColor="#ff3448" stopOpacity={0.2} />
|
||||
<stop offset="100%" stopColor="#ff3448" stopOpacity={0.02} />
|
||||
<stop offset="0%" stopColor={MATERIAL_ACCENTS.secondary} stopOpacity={0.22} />
|
||||
<stop offset="100%" stopColor={MATERIAL_ACCENTS.secondary} stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke="#39d5ff" fill="url(#trendSpend)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="cans" name="Cans" stroke="#ff3448" fill="url(#trendCans)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="spend" name="Spend" stroke={MATERIAL_ACCENTS.primary} fill="url(#trendSpend)" strokeWidth={3} />
|
||||
<Area type="monotone" dataKey="cans" name="Cans" stroke={MATERIAL_ACCENTS.secondary} fill="url(#trendCans)" strokeWidth={3} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
@@ -1303,11 +1328,11 @@ function TrendsView({
|
||||
{chartData.length ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Bar dataKey="caffeine" name="Caffeine" fill="#39d5ff" radius={[8, 8, 0, 0]} />
|
||||
<Bar dataKey="caffeine" name="Caffeine" fill={MATERIAL_ACCENTS.primary} radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
@@ -1319,12 +1344,12 @@ function TrendsView({
|
||||
{weekData.length ? (
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<RechartsLineChart data={weekData} margin={{ top: 12, right: 16, bottom: 0, left: -12 }}>
|
||||
<CartesianGrid stroke="rgba(203,213,225,0.12)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="#94a3b8" tickLine={false} axisLine={false} />
|
||||
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<YAxis stroke="var(--subtle)" tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Line type="monotone" dataKey="spend" name="Spend" stroke="#ffd84d" strokeWidth={3} dot={{ r: 3 }} />
|
||||
<Line type="monotone" dataKey="cans" name="Cans" stroke="#ffb7d9" strokeWidth={3} dot={{ r: 3 }} />
|
||||
<Line type="monotone" dataKey="spend" name="Spend" stroke={MATERIAL_ACCENTS.tertiary} strokeWidth={3} dot={{ r: 3 }} />
|
||||
<Line type="monotone" dataKey="cans" name="Cans" stroke={MATERIAL_ACCENTS.primary} strokeWidth={3} dot={{ r: 3 }} />
|
||||
</RechartsLineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
@@ -1338,7 +1363,7 @@ function TrendsView({
|
||||
{flavourData.length ? (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<PieChart>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={76} outerRadius={118} paddingAngle={4} stroke="#080d1f" strokeWidth={4}>
|
||||
<Pie data={flavourData} dataKey="value" nameKey="name" innerRadius={76} outerRadius={118} paddingAngle={4} stroke="var(--surface-container)" strokeWidth={4}>
|
||||
{flavourData.map((entry) => (
|
||||
<Cell key={entry.name} fill={entry.accent} />
|
||||
))}
|
||||
@@ -1384,9 +1409,9 @@ function DataView({
|
||||
<div className="grid gap-4 xl:grid-cols-[1fr_0.85fr]">
|
||||
<AppCard title="Appwrite storage" subtitle={`${entries.length} entries synced for this user`}>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<MiniMetric label="All-time cans" value={dashboard.allTimeCans} accent="#39d5ff" />
|
||||
<MiniMetric label="Total spend" value={dashboard.totalSpend} accent="#ffd84d" />
|
||||
<MiniMetric label="Favourite" value={dashboard.favouriteFlavour} accent="#ffb7d9" />
|
||||
<MiniMetric label="All-time cans" value={dashboard.allTimeCans} accent={MATERIAL_ACCENTS.primary} />
|
||||
<MiniMetric label="Total spend" value={dashboard.totalSpend} accent={MATERIAL_ACCENTS.tertiary} />
|
||||
<MiniMetric label="Favourite" value={dashboard.favouriteFlavour} accent={MATERIAL_ACCENTS.secondary} />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@@ -1425,7 +1450,7 @@ function DataView({
|
||||
</AppCard>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<AppCard title="Excel theme" subtitle="Pastel pink and Miku blue workbook">
|
||||
<AppCard title="Excel theme" subtitle="Pastel pink and soft blue workbook">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-lg border border-cyan-200/30 bg-cyan-200/10 p-4">
|
||||
<FileSpreadsheet className="text-cyan-100" size={24} aria-hidden="true" />
|
||||
@@ -1783,7 +1808,7 @@ function EntryModal({
|
||||
const initialFlavour = entry?.flavour ?? DEFAULT_FLAVOUR.name;
|
||||
const [selectedFlavour, setSelectedFlavour] = useState(initialFlavour);
|
||||
const [customFlavour, setCustomFlavour] = useState("");
|
||||
const [customAccent, setCustomAccent] = useState("#39d5ff");
|
||||
const [customAccent, setCustomAccent] = useState(MATERIAL_ACCENTS.custom);
|
||||
const [cans, setCans] = useState(entry?.cans.toString() ?? "1");
|
||||
const [sizePreset, setSizePreset] = useState(sizeToPreset(entry?.sizeMl ?? 250));
|
||||
const [customSize, setCustomSize] = useState(entry?.sizeMl.toString() ?? "250");
|
||||
@@ -1799,7 +1824,7 @@ function EntryModal({
|
||||
const editingCustom = entry && !BUILT_IN_FLAVOURS.some((flavour) => flavour.name === entry.flavour);
|
||||
setSelectedFlavour(editingCustom ? entry.flavour : entry?.flavour ?? DEFAULT_FLAVOUR.name);
|
||||
setCustomFlavour(editingCustom ? entry.flavour : "");
|
||||
setCustomAccent(entry?.flavourAccent ?? "#39d5ff");
|
||||
setCustomAccent(entry?.flavourAccent ?? MATERIAL_ACCENTS.custom);
|
||||
setCans(entry?.cans.toString() ?? "1");
|
||||
setSizePreset(sizeToPreset(entry?.sizeMl ?? 250));
|
||||
setCustomSize(entry?.sizeMl.toString() ?? "250");
|
||||
@@ -2057,9 +2082,9 @@ function ImportPreviewModal({
|
||||
</div>
|
||||
|
||||
<div className="mb-4 grid gap-3 sm:grid-cols-3">
|
||||
<MiniMetric label="Ready" value={`${validRows.length}`} accent="#39d5ff" />
|
||||
<MiniMetric label="Duplicates" value={`${duplicateRows.length}`} accent="#ffd84d" />
|
||||
<MiniMetric label="Invalid" value={`${invalidRows.length}`} accent="#ff3448" />
|
||||
<MiniMetric label="Ready" value={`${validRows.length}`} accent={MATERIAL_ACCENTS.primary} />
|
||||
<MiniMetric label="Duplicates" value={`${duplicateRows.length}`} accent={MATERIAL_ACCENTS.tertiary} />
|
||||
<MiniMetric label="Invalid" value={`${invalidRows.length}`} accent={MATERIAL_ACCENTS.error} />
|
||||
</div>
|
||||
|
||||
<div className="max-h-[48vh] overflow-auto rounded-lg border border-white/10">
|
||||
@@ -2306,8 +2331,7 @@ function actionLabel(value: string) {
|
||||
}
|
||||
|
||||
function readStoredAccent(): AccentTheme {
|
||||
const value = localStorage.getItem(ACCENT_STORAGE_KEY);
|
||||
return value === "pink" ? "pink" : "blue";
|
||||
return "pink";
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
+453
-114
@@ -2,10 +2,22 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Google Sans";
|
||||
src: local("Google Sans"), local("GoogleSans-Regular");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Google Sans Text";
|
||||
src: local("Google Sans Text"), local("GoogleSansText-Regular");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Avenir Next", "Helvetica Neue", sans-serif;
|
||||
background: #f5fbff;
|
||||
font-family: "Google Sans", "Google Sans Text", "Product Sans", Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fff8fb;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -14,18 +26,18 @@
|
||||
|
||||
html {
|
||||
min-width: 320px;
|
||||
background: #f5fbff;
|
||||
background: #fff8fb;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: #f5fbff;
|
||||
color: #193042;
|
||||
font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Avenir Next", "Helvetica Neue", sans-serif;
|
||||
background: #fff8fb;
|
||||
color: #21191d;
|
||||
font-family: "Google Sans", "Google Sans Text", "Product Sans", Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: geometricPrecision;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -39,12 +51,12 @@ button:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--accent-strong, #74c7ec);
|
||||
outline: 2px solid var(--primary, #9c4168);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in srgb, var(--accent, #bdeeff) 55%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-container, #ffd8e7) 68%, transparent);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -53,83 +65,293 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(230, 244, 255, 0.92);
|
||||
background: var(--surface-container-low, #fff0f5);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(116, 155, 184, 0.45);
|
||||
background: color-mix(in srgb, var(--outline, #85737a) 42%, transparent);
|
||||
border: 3px solid var(--surface-container-low, #fff0f5);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.glass-panel {
|
||||
@apply rounded-lg border shadow-fridge backdrop-blur-2xl;
|
||||
background: color-mix(in srgb, var(--panel) 86%, white);
|
||||
border-color: var(--border);
|
||||
.auth-layout {
|
||||
@apply mx-auto grid min-h-screen w-full max-w-6xl gap-6 px-4 py-8 lg:grid-cols-[1.05fr_0.95fr];
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.can-panel {
|
||||
@apply rounded-lg border shadow-cyan backdrop-blur-2xl;
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in srgb, var(--accent) 42%, white), rgba(255, 255, 255, 0.96) 52%, color-mix(in srgb, var(--accent-soft) 76%, white));
|
||||
border-color: var(--border);
|
||||
.auth-hero {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.can-emblem {
|
||||
@apply flex h-11 w-11 items-center justify-center rounded-lg border text-[#193042] shadow-cyan;
|
||||
background: linear-gradient(135deg, var(--accent), #ffffff 58%, var(--accent-warm));
|
||||
border-color: color-mix(in srgb, var(--accent-strong) 35%, white);
|
||||
.auth-signal-grid {
|
||||
@apply mt-6 grid gap-3 sm:grid-cols-3;
|
||||
}
|
||||
|
||||
.command-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 font-display text-sm font-semibold shadow-sm transition active:scale-[0.99];
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
.auth-panel {
|
||||
@apply border p-5 shadow-fridge sm:p-6;
|
||||
background: color-mix(in srgb, var(--surface-container) 88%, white);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold text-[#193042] shadow-cyan transition disabled:cursor-not-allowed;
|
||||
background: var(--accent);
|
||||
border-color: color-mix(in srgb, var(--accent-strong) 42%, white);
|
||||
.state-chip {
|
||||
@apply inline-flex min-h-10 items-center gap-2 px-3 text-sm font-semibold;
|
||||
background: var(--primary-container);
|
||||
border-radius: 999px;
|
||||
color: var(--on-primary-container);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-semibold shadow-sm transition disabled:cursor-not-allowed;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
.segmented-control {
|
||||
@apply grid grid-cols-2 gap-1 border p-1;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.excel-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold text-[#193042] shadow-cyan transition hover:brightness-105 disabled:cursor-not-allowed;
|
||||
background: linear-gradient(135deg, #ffe1ef, var(--accent));
|
||||
border-color: color-mix(in srgb, var(--accent-strong) 35%, white);
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border border-red-300 bg-red-500/90 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-red-400 disabled:cursor-not-allowed disabled:border-slate-300 disabled:bg-slate-200 disabled:text-slate-500;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply flex min-h-11 items-center gap-3 rounded-md px-3 text-sm font-medium transition;
|
||||
.segmented-control button {
|
||||
@apply min-h-10 px-3 text-sm font-semibold transition;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.segmented-control-active {
|
||||
background: var(--primary-container);
|
||||
color: var(--on-primary-container) !important;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
@apply mx-auto grid w-full gap-4 px-3 pb-28 pt-3;
|
||||
max-width: 1720px;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
.material-drawer {
|
||||
@apply sticky top-6 hidden h-[calc(100vh-3rem)] flex-col border p-4 lg:flex;
|
||||
background: var(--surface-container);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
.drawer-brand {
|
||||
@apply mb-5 flex items-center gap-3 px-1;
|
||||
}
|
||||
|
||||
.drawer-primary-action {
|
||||
@apply mb-5 inline-flex min-h-14 items-center justify-center gap-3 px-5 text-sm font-semibold shadow-can transition active:scale-[0.99];
|
||||
background: var(--primary-container);
|
||||
border-radius: 18px;
|
||||
color: var(--on-primary-container);
|
||||
}
|
||||
|
||||
.drawer-nav {
|
||||
@apply grid gap-2;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
@apply mt-auto grid gap-3;
|
||||
}
|
||||
|
||||
.drawer-info-card {
|
||||
@apply border p-4;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.top-app-bar {
|
||||
@apply border p-4 sm:p-5;
|
||||
background: color-mix(in srgb, var(--surface-container-low) 84%, white);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
.top-app-bar-main {
|
||||
@apply flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between;
|
||||
}
|
||||
|
||||
.top-title-cluster {
|
||||
@apply flex min-w-0 items-start gap-3;
|
||||
}
|
||||
|
||||
.top-app-icon {
|
||||
@apply mt-1 flex h-12 w-12 shrink-0 items-center justify-center;
|
||||
background: var(--primary-container);
|
||||
border-radius: 16px;
|
||||
color: var(--on-primary-container);
|
||||
}
|
||||
|
||||
.top-kicker {
|
||||
@apply text-sm font-medium;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.top-title {
|
||||
@apply mt-1 break-words text-4xl font-semibold sm:text-5xl;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.top-meta-row {
|
||||
@apply flex flex-wrap items-center gap-2;
|
||||
}
|
||||
|
||||
.account-chip {
|
||||
@apply inline-flex min-h-10 max-w-full items-center rounded-md px-3 text-xs font-semibold;
|
||||
background: var(--surface-container-high);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.top-action-row {
|
||||
@apply mt-5 flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between;
|
||||
}
|
||||
|
||||
.top-action-primary,
|
||||
.top-action-secondary {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
|
||||
.mobile-nav-bar {
|
||||
@apply fixed inset-x-3 bottom-3 z-40 grid grid-cols-4 gap-1 border p-1 shadow-fridge;
|
||||
background: color-mix(in srgb, var(--surface-container-high) 92%, white);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
@apply flex min-h-16 flex-col items-center justify-center gap-1 text-[11px] font-semibold transition;
|
||||
border-radius: 22px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.mobile-nav-item-active {
|
||||
background: var(--primary-container);
|
||||
color: var(--on-primary-container) !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.app-layout {
|
||||
grid-template-columns: 300px minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
@apply rounded-lg border shadow-fridge;
|
||||
background: var(--surface-container);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.can-panel {
|
||||
@apply rounded-lg border shadow-can;
|
||||
background: linear-gradient(135deg, var(--primary-container), var(--surface-container-high) 58%, var(--tertiary-container));
|
||||
border-color: color-mix(in srgb, var(--primary) 20%, var(--outline-variant));
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.can-emblem {
|
||||
@apply flex h-11 w-11 items-center justify-center rounded-lg border shadow-can;
|
||||
background: var(--primary-container);
|
||||
border-color: color-mix(in srgb, var(--primary) 24%, var(--outline-variant));
|
||||
color: var(--on-primary-container);
|
||||
}
|
||||
|
||||
.command-button,
|
||||
.secondary-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold shadow-sm transition active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
background: var(--secondary-container);
|
||||
border-color: transparent;
|
||||
color: var(--on-secondary-container);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold shadow-can transition active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
background: var(--primary);
|
||||
border-color: transparent;
|
||||
color: var(--on-primary);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.excel-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold shadow-sm transition active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
background: var(--tertiary-container);
|
||||
border-color: transparent;
|
||||
color: var(--on-tertiary-container);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.primary-button:hover,
|
||||
.secondary-button:hover,
|
||||
.command-button:hover,
|
||||
.excel-button:hover,
|
||||
.icon-button:hover,
|
||||
.quick-add:hover,
|
||||
.list-button:hover {
|
||||
filter: brightness(0.985);
|
||||
box-shadow: var(--elevation-2);
|
||||
}
|
||||
|
||||
.primary-button:disabled,
|
||||
.secondary-button:disabled,
|
||||
.command-button:disabled,
|
||||
.excel-button:disabled,
|
||||
.danger-button:disabled {
|
||||
box-shadow: none;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.danger-button {
|
||||
@apply inline-flex min-h-11 items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold shadow-sm transition active:scale-[0.99] disabled:cursor-not-allowed;
|
||||
background: var(--error);
|
||||
border-color: transparent;
|
||||
color: var(--on-error);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply flex min-h-12 items-center gap-3 border border-transparent px-4 text-sm font-medium transition;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--surface-container-high);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-item-active {
|
||||
@apply shadow-cyan;
|
||||
background: var(--accent);
|
||||
color: var(--text) !important;
|
||||
background: var(--primary-container);
|
||||
color: var(--on-primary-container) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
@apply inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md border bg-white shadow-sm transition;
|
||||
border-color: var(--border);
|
||||
@apply inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md border shadow-sm transition;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.quick-add {
|
||||
@apply inline-flex min-h-12 items-center gap-2 rounded-md border bg-white px-3 font-display text-sm font-semibold shadow-sm transition hover:border-[var(--accent)];
|
||||
@apply inline-flex min-h-12 items-center gap-2 rounded-md border px-3 text-sm font-semibold shadow-sm transition;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@@ -138,45 +360,65 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.field-control {
|
||||
@apply w-full rounded-md border bg-white px-3 py-3 text-base font-normal shadow-sm transition;
|
||||
border-color: var(--border);
|
||||
@apply w-full rounded-md border px-3 py-3 text-base font-normal shadow-sm transition;
|
||||
background: var(--surface-container-lowest);
|
||||
border-color: var(--outline-variant);
|
||||
color: var(--text);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.field-control:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent);
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
@apply max-h-[92vh] w-full max-w-3xl overflow-y-auto rounded-lg border bg-white/95 p-5 shadow-fridge backdrop-blur-2xl sm:p-6;
|
||||
border-color: var(--border);
|
||||
@apply max-h-[92vh] w-full max-w-3xl overflow-y-auto rounded-lg border p-5 shadow-fridge sm:p-6;
|
||||
background: var(--surface-container-lowest);
|
||||
border-color: var(--outline-variant);
|
||||
color: var(--text);
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
@apply grid gap-3 rounded-lg border bg-white/80 p-4 transition sm:grid-cols-[1fr_auto] sm:items-center;
|
||||
border-color: var(--border);
|
||||
@apply grid gap-3 rounded-lg border p-4 transition sm:grid-cols-[1fr_auto] sm:items-center;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.entry-row:hover {
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
.list-button {
|
||||
@apply flex min-h-11 items-center justify-between rounded-lg border bg-white px-3 text-sm font-semibold transition;
|
||||
border-color: var(--border);
|
||||
color: var(--accent-strong);
|
||||
@apply flex min-h-11 items-center justify-between rounded-lg border px-3 text-sm font-semibold transition;
|
||||
background: var(--secondary-container);
|
||||
border-color: transparent;
|
||||
color: var(--on-secondary-container);
|
||||
}
|
||||
|
||||
.status-card {
|
||||
@apply flex items-center gap-2 rounded-md border px-3 py-2 text-sm shadow-sm backdrop-blur-xl;
|
||||
@apply flex items-center gap-2 rounded-md border px-3 py-2 text-sm shadow-sm;
|
||||
}
|
||||
|
||||
.accent-picker {
|
||||
@apply inline-flex min-h-11 items-center gap-1 rounded-md border bg-white/80 p-1 shadow-sm;
|
||||
border-color: var(--border);
|
||||
@apply inline-flex min-h-11 items-center gap-1 rounded-md border p-1 shadow-sm;
|
||||
background: var(--surface-container-high);
|
||||
border-color: var(--outline-variant);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.accent-picker button {
|
||||
@apply inline-flex min-h-9 items-center gap-2 rounded px-3 text-sm font-semibold transition;
|
||||
color: var(--muted);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.accent-picker button:hover,
|
||||
.accent-picker-active {
|
||||
background: var(--accent-soft);
|
||||
color: var(--text) !important;
|
||||
background: var(--primary-container);
|
||||
color: var(--on-primary-container) !important;
|
||||
}
|
||||
|
||||
.accent-swatch {
|
||||
@@ -184,58 +426,117 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.accent-swatch-blue {
|
||||
background: #bdeeff;
|
||||
background: #d8e7ff;
|
||||
}
|
||||
|
||||
.accent-swatch-pink {
|
||||
background: #ffd6e8;
|
||||
background: #ffd8e7;
|
||||
}
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
--accent: #bdeeff;
|
||||
--accent-soft: #e7f8ff;
|
||||
--accent-strong: #4aa8d6;
|
||||
--accent-warm: #ffe2ef;
|
||||
--bg: #f5fbff;
|
||||
--panel: #f8fcff;
|
||||
--panel-strong: #ffffff;
|
||||
--border: rgba(104, 164, 198, 0.24);
|
||||
--text: #193042;
|
||||
--muted: #607587;
|
||||
--subtle: #7e93a3;
|
||||
--primary: #9c4168;
|
||||
--on-primary: #ffffff;
|
||||
--primary-container: #ffd8e7;
|
||||
--on-primary-container: #3e001d;
|
||||
--secondary: #526354;
|
||||
--on-secondary: #ffffff;
|
||||
--secondary-container: #d7efe2;
|
||||
--on-secondary-container: #102116;
|
||||
--tertiary: #765930;
|
||||
--on-tertiary: #ffffff;
|
||||
--tertiary-container: #ffddb2;
|
||||
--on-tertiary-container: #2b1700;
|
||||
--error: #ba1a1a;
|
||||
--on-error: #ffffff;
|
||||
--error-container: #ffdad6;
|
||||
--on-error-container: #410002;
|
||||
--bg: #fff8fb;
|
||||
--surface: #fff8fb;
|
||||
--surface-container-lowest: #ffffff;
|
||||
--surface-container-low: #fff0f5;
|
||||
--surface-container: #faedf3;
|
||||
--surface-container-high: #f4e7ee;
|
||||
--panel: var(--surface-container);
|
||||
--panel-strong: var(--surface-container-lowest);
|
||||
--outline: #85737a;
|
||||
--outline-variant: #d8c2ca;
|
||||
--text: #21191d;
|
||||
--muted: #655b60;
|
||||
--subtle: #83757c;
|
||||
--accent: var(--primary-container);
|
||||
--accent-soft: var(--surface-container-low);
|
||||
--accent-strong: var(--primary);
|
||||
--accent-warm: #d7efe2;
|
||||
--chart-primary: #b85d84;
|
||||
--chart-secondary: #5f7f6f;
|
||||
--chart-tertiary: #906d1d;
|
||||
--chart-error: #ba1a1a;
|
||||
--chart-grid: rgba(132, 115, 122, 0.24);
|
||||
--elevation-1: 0 1px 2px rgba(69, 54, 62, 0.14), 0 2px 6px rgba(69, 54, 62, 0.08);
|
||||
--elevation-2: 0 2px 6px rgba(69, 54, 62, 0.14), 0 8px 18px rgba(69, 54, 62, 0.08);
|
||||
min-height: 100vh;
|
||||
background: var(--bg) !important;
|
||||
color: var(--text) !important;
|
||||
font-family: "Google Sans", "Google Sans Text", "Product Sans", Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.app-shell[data-accent="blue"] {
|
||||
--primary: #49617d;
|
||||
--on-primary: #ffffff;
|
||||
--primary-container: #d8e7ff;
|
||||
--on-primary-container: #061d35;
|
||||
--secondary: #5f5d72;
|
||||
--secondary-container: #e5dff9;
|
||||
--on-secondary-container: #1c1a2c;
|
||||
--tertiary: #765930;
|
||||
--tertiary-container: #ffddb2;
|
||||
--bg: #f8fbff;
|
||||
--surface: #f8fbff;
|
||||
--surface-container-lowest: #ffffff;
|
||||
--surface-container-low: #eef4fb;
|
||||
--surface-container: #e8eef6;
|
||||
--surface-container-high: #e1e9f2;
|
||||
--outline: #72777f;
|
||||
--outline-variant: #c2c7cf;
|
||||
--text: #191c20;
|
||||
--muted: #5d6269;
|
||||
--subtle: #777c83;
|
||||
--accent-warm: #ffd8e7;
|
||||
--chart-primary: #49617d;
|
||||
--chart-secondary: #9c4168;
|
||||
--chart-tertiary: #906d1d;
|
||||
--chart-error: #ba1a1a;
|
||||
--chart-grid: rgba(114, 119, 127, 0.24);
|
||||
}
|
||||
|
||||
.app-shell[data-accent="pink"] {
|
||||
--accent: #ffd6e8;
|
||||
--accent-soft: #fff0f7;
|
||||
--accent-strong: #d46c9d;
|
||||
--accent-warm: #dff6ff;
|
||||
--bg: #fff8fc;
|
||||
--panel: #fffbfd;
|
||||
--border: rgba(210, 108, 157, 0.22);
|
||||
--primary: #9c4168;
|
||||
--on-primary: #ffffff;
|
||||
--primary-container: #ffd8e7;
|
||||
--on-primary-container: #3e001d;
|
||||
}
|
||||
|
||||
.backdrop-wash {
|
||||
background:
|
||||
radial-gradient(circle at 14% 12%, color-mix(in srgb, var(--accent) 48%, transparent), transparent 32%),
|
||||
radial-gradient(circle at 82% 6%, color-mix(in srgb, var(--accent-warm) 72%, transparent), transparent 34%),
|
||||
linear-gradient(180deg, var(--bg) 0%, #ffffff 46%, color-mix(in srgb, var(--accent-soft) 66%, white) 100%);
|
||||
background: linear-gradient(180deg, var(--bg) 0%, var(--surface-container-lowest) 46%, var(--surface-container-low) 100%);
|
||||
}
|
||||
|
||||
.backdrop-grid {
|
||||
background-image:
|
||||
linear-gradient(color-mix(in srgb, var(--accent-strong) 12%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, color-mix(in srgb, var(--accent-strong) 12%, transparent) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
opacity: 0.5;
|
||||
linear-gradient(var(--chart-grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--chart-grid) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
opacity: 0.22;
|
||||
}
|
||||
|
||||
.backdrop-rail {
|
||||
background: linear-gradient(90deg, var(--accent), #ffffff, var(--accent-warm), var(--accent));
|
||||
background: linear-gradient(90deg, var(--primary-container), var(--accent-warm), var(--tertiary-container), var(--primary-container));
|
||||
}
|
||||
|
||||
.app-shell *,
|
||||
.app-shell .tracking-tight,
|
||||
.app-shell [class*="tracking-"] {
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.app-shell .text-white,
|
||||
@@ -254,46 +555,77 @@ textarea:focus-visible {
|
||||
|
||||
.app-shell .text-cyan-50,
|
||||
.app-shell .text-cyan-100,
|
||||
.app-shell .text-cyan-200 {
|
||||
color: var(--accent-strong) !important;
|
||||
.app-shell .text-cyan-200,
|
||||
.app-shell .text-pink-100,
|
||||
.app-shell .text-pink-200 {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.app-shell .text-emerald-200 {
|
||||
color: #16845c !important;
|
||||
color: #2d6f57 !important;
|
||||
}
|
||||
|
||||
.app-shell .text-amber-100,
|
||||
.app-shell .text-amber-200 {
|
||||
color: #8a5a00 !important;
|
||||
color: #765930 !important;
|
||||
}
|
||||
|
||||
.app-shell .text-red-100,
|
||||
.app-shell .text-red-200 {
|
||||
color: #b4233c !important;
|
||||
color: var(--error) !important;
|
||||
}
|
||||
|
||||
.app-shell .text-\[\#07101f\] {
|
||||
color: var(--text) !important;
|
||||
color: var(--on-primary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .primary-button,
|
||||
.app-shell .primary-button * {
|
||||
color: var(--on-primary) !important;
|
||||
}
|
||||
|
||||
.app-shell .secondary-button,
|
||||
.app-shell .secondary-button *,
|
||||
.app-shell .command-button,
|
||||
.app-shell .command-button * {
|
||||
color: var(--on-secondary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .excel-button,
|
||||
.app-shell .excel-button * {
|
||||
color: var(--on-tertiary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-\[\#050711\],
|
||||
.app-shell .bg-\[\#090f22\]\/90,
|
||||
.app-shell .bg-\[\#0d142c\],
|
||||
.app-shell .bg-\[\#080d1f\]\/95,
|
||||
.app-shell .bg-\[\#070d1f\]\/90 {
|
||||
background: var(--panel-strong) !important;
|
||||
.app-shell .bg-\[\#070d1f\]\/90,
|
||||
.app-shell .bg-\[\#07101f\] {
|
||||
background: var(--surface-container-high) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-cyan-300,
|
||||
.app-shell .bg-cyan-200,
|
||||
.app-shell .bg-cyan-300\/10,
|
||||
.app-shell .bg-cyan-200\/10 {
|
||||
background-color: var(--accent) !important;
|
||||
.app-shell .bg-cyan-200\/10,
|
||||
.app-shell .bg-cyan-300\/15 {
|
||||
background-color: var(--primary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-pink-200,
|
||||
.app-shell .bg-pink-200\/10 {
|
||||
background-color: var(--accent-soft) !important;
|
||||
background-color: var(--secondary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-amber-300\/10,
|
||||
.app-shell .bg-amber-300\/15 {
|
||||
background-color: var(--tertiary-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-red-500\/10,
|
||||
.app-shell .bg-red-500\/15 {
|
||||
background-color: var(--error-container) !important;
|
||||
}
|
||||
|
||||
.app-shell .bg-white\/5,
|
||||
@@ -305,7 +637,7 @@ textarea:focus-visible {
|
||||
.app-shell .hover\:bg-white\/10:hover,
|
||||
.app-shell .hover\:bg-white\/\[0\.10\]:hover,
|
||||
.app-shell .hover\:bg-white\/\[0\.12\]:hover {
|
||||
background-color: color-mix(in srgb, var(--accent-soft) 52%, white) !important;
|
||||
background-color: var(--surface-container-high) !important;
|
||||
}
|
||||
|
||||
.app-shell .border-white\/10,
|
||||
@@ -313,26 +645,33 @@ textarea:focus-visible {
|
||||
.app-shell .border-cyan-200\/25,
|
||||
.app-shell .border-cyan-200\/30,
|
||||
.app-shell .border-cyan-300\/30,
|
||||
.app-shell .border-cyan-300\/40,
|
||||
.app-shell .border-pink-200\/30,
|
||||
.app-shell .border-pink-200\/40 {
|
||||
border-color: var(--border) !important;
|
||||
.app-shell .border-pink-200\/40,
|
||||
.app-shell .border-amber-300\/40,
|
||||
.app-shell .border-red-400\/40 {
|
||||
border-color: var(--outline-variant) !important;
|
||||
}
|
||||
|
||||
.app-shell .shadow-fridge,
|
||||
.app-shell .shadow-cyan,
|
||||
.app-shell .shadow-sm {
|
||||
box-shadow: 0 18px 55px rgba(83, 139, 174, 0.14), 0 1px 2px rgba(83, 139, 174, 0.10) !important;
|
||||
box-shadow: var(--elevation-1) !important;
|
||||
}
|
||||
|
||||
.app-shell .danger-button,
|
||||
.app-shell .danger-button * {
|
||||
color: #ffffff !important;
|
||||
color: var(--on-error) !important;
|
||||
}
|
||||
|
||||
.app-shell .field-control::placeholder {
|
||||
color: color-mix(in srgb, var(--muted) 58%, white);
|
||||
}
|
||||
|
||||
.app-shell input[type="checkbox"] {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.app-shell .modal-panel {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
+13
-12
@@ -6,20 +6,21 @@ export default {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
display: [
|
||||
"SF Pro Display",
|
||||
"SF Pro Text",
|
||||
"Google Sans",
|
||||
"Google Sans Text",
|
||||
"Product Sans",
|
||||
"Roboto",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Avenir Next",
|
||||
"Helvetica Neue",
|
||||
"sans-serif",
|
||||
],
|
||||
body: [
|
||||
"SF Pro Text",
|
||||
"Google Sans",
|
||||
"Google Sans Text",
|
||||
"Product Sans",
|
||||
"Roboto",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Avenir Next",
|
||||
"Helvetica Neue",
|
||||
"sans-serif",
|
||||
],
|
||||
},
|
||||
@@ -38,11 +39,11 @@ export default {
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
apple: "0 18px 55px rgba(0, 0, 0, 0.22), 0 1px 2px rgba(0, 0, 0, 0.18)",
|
||||
fridge: "0 18px 70px rgba(0, 0, 0, 0.34), 0 1px 2px rgba(255, 255, 255, 0.06)",
|
||||
can: "0 10px 24px rgba(57, 213, 255, 0.12)",
|
||||
redline: "0 12px 28px rgba(255, 52, 72, 0.26)",
|
||||
cyan: "0 14px 32px rgba(57, 213, 255, 0.18)",
|
||||
apple: "0 1px 2px rgba(69, 54, 62, 0.14), 0 2px 6px rgba(69, 54, 62, 0.08)",
|
||||
fridge: "0 2px 6px rgba(69, 54, 62, 0.12), 0 8px 18px rgba(69, 54, 62, 0.08)",
|
||||
can: "0 1px 2px rgba(156, 65, 104, 0.18), 0 3px 8px rgba(156, 65, 104, 0.10)",
|
||||
redline: "0 2px 8px rgba(186, 26, 26, 0.20)",
|
||||
cyan: "0 1px 2px rgba(156, 65, 104, 0.16), 0 4px 12px rgba(156, 65, 104, 0.10)",
|
||||
},
|
||||
backgroundImage: {
|
||||
"carbon-grid":
|
||||
|
||||
Reference in New Issue
Block a user