FIRE calculator
This commit is contained in:
parent
fe03807739
commit
31415c10a2
291
src/app/components/FireCalculatorForm.tsx
Normal file
291
src/app/components/FireCalculatorForm.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "../../components/ui/form";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../../components/ui/card";
|
||||||
|
|
||||||
|
// Schema for form validation
|
||||||
|
const formSchema = z.object({
|
||||||
|
startingCapital: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, "Starting capital must be a non-negative number"),
|
||||||
|
monthlySavings: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, "Monthly savings must be a non-negative number"),
|
||||||
|
currentAge: z.coerce.number().min(18, "Age must be at least 18"),
|
||||||
|
cagr: z.coerce.number().min(0, "Growth rate must be a non-negative number"),
|
||||||
|
desiredMonthlyAllowance: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, "Monthly allowance must be a non-negative number"),
|
||||||
|
swr: z.coerce.number().min(0.1, "Withdrawal rate must be at least 0.1%"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type for form values
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface CalculationResult {
|
||||||
|
fireNumber: number | null;
|
||||||
|
retirementAge: number | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FireCalculatorForm() {
|
||||||
|
const [result, setResult] = useState<CalculationResult | null>(null);
|
||||||
|
|
||||||
|
// Initialize form with default values
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
startingCapital: 10000,
|
||||||
|
monthlySavings: 500,
|
||||||
|
currentAge: 30,
|
||||||
|
cagr: 7,
|
||||||
|
desiredMonthlyAllowance: 2000,
|
||||||
|
swr: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(values: FormValues) {
|
||||||
|
setResult(null); // Reset previous results
|
||||||
|
|
||||||
|
const sc = values.startingCapital;
|
||||||
|
const ms = values.monthlySavings;
|
||||||
|
const ca = values.currentAge;
|
||||||
|
const annualRate = values.cagr / 100;
|
||||||
|
const monthlyAllowance = values.desiredMonthlyAllowance;
|
||||||
|
const safeWithdrawalRate = values.swr / 100;
|
||||||
|
|
||||||
|
// Calculate FIRE number (the amount needed for retirement)
|
||||||
|
const fireNumber = (monthlyAllowance * 12) / safeWithdrawalRate;
|
||||||
|
|
||||||
|
let currentCapital = sc;
|
||||||
|
let age = ca;
|
||||||
|
const monthlyRate = Math.pow(1 + annualRate, 1 / 12) - 1;
|
||||||
|
const maxYears = 100; // Set a limit to prevent infinite loops
|
||||||
|
|
||||||
|
if (currentCapital >= fireNumber) {
|
||||||
|
setResult({ fireNumber, retirementAge: age });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let year = 0; year < maxYears; year++) {
|
||||||
|
const capitalAtYearStart = currentCapital;
|
||||||
|
for (let month = 0; month < 12; month++) {
|
||||||
|
currentCapital += ms;
|
||||||
|
currentCapital *= 1 + monthlyRate;
|
||||||
|
}
|
||||||
|
age++;
|
||||||
|
|
||||||
|
if (currentCapital >= fireNumber) {
|
||||||
|
setResult({ fireNumber, retirementAge: age });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent infinite loop if savings don't outpace growth required
|
||||||
|
if (currentCapital <= capitalAtYearStart && ms <= 0) {
|
||||||
|
setResult({
|
||||||
|
fireNumber: null,
|
||||||
|
retirementAge: null,
|
||||||
|
error: "Cannot reach FIRE goal with current savings and growth rate.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If loop finishes without reaching FIRE number
|
||||||
|
setResult({
|
||||||
|
fireNumber: null,
|
||||||
|
retirementAge: null,
|
||||||
|
error: `Could not reach FIRE goal within ${maxYears.toString()} years.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format currency
|
||||||
|
const formatCurrency = (value: number | null) => {
|
||||||
|
if (value === null) return "N/A";
|
||||||
|
return new Intl.NumberFormat("en", {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-3xl">
|
||||||
|
<Card className="mb-8">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl">FIRE Calculator</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Calculate your path to financial independence and retirement
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="startingCapital"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Starting Capital</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 10000"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="monthlySavings"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Monthly Savings</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 500"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="currentAge"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Current Age</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 30"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cagr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Expected Annual Growth Rate (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 7"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="desiredMonthlyAllowance"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Desired Monthly Allowance in Retirement
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 2000"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="swr"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Safe Withdrawal Rate (%)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g., 4"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Calculate
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Results</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{result.error ? (
|
||||||
|
<p className="text-destructive">{result.error}</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>FIRE Number (Required Capital)</Label>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{formatCurrency(result.fireNumber)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Estimated Retirement Age</Label>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{result.retirementAge ?? "N/A"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
25
src/app/layout.tsx
Normal file
25
src/app/layout.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { type Metadata } from "next";
|
||||||
|
import { Geist } from "next/font/google";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create T3 App",
|
||||||
|
description: "Generated by create-t3-app",
|
||||||
|
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const geist = Geist({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={`${geist.variable}`}>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
14
src/app/page.tsx
Normal file
14
src/app/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import FireCalculatorForm from "./components/FireCalculatorForm";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[oklch(0.49_0.1326_259.29)] to-[oklch(0.33_0.1316_336.24)] p-4 text-[oklch(0.97_0.0228_95.96)]">
|
||||||
|
<div className="container mx-auto flex flex-col items-center justify-center gap-12 px-4 py-16">
|
||||||
|
<h1 className="text-primary-foreground text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||||
|
FIRE Calculator
|
||||||
|
</h1>
|
||||||
|
<FireCalculatorForm />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(...inputs));
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
--font-sans:
|
||||||
|
var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,71 +49,77 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.67 0.0763 198.81 / 20%); /* verdigris with opacity */
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(
|
||||||
--accent: oklch(0.97 0 0);
|
0.39 0.0215 96.47 / 80%
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
); /* black olive with opacity */
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--border: oklch(0.922 0 0);
|
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--input: oklch(0.922 0 0);
|
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||||
--ring: oklch(0.708 0 0);
|
--border: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--input: oklch(0.67 0.0763 198.81 / 30%); /* verdigris with opacity */
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||||
--sidebar: oklch(0.985 0 0);
|
--chart-4: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar: oklch(0.49 0.1326 259.29 / 10%); /* denim with opacity */
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
|
--sidebar-border: oklch(
|
||||||
|
0.67 0.0763 198.81 / 20%
|
||||||
|
); /* verdigris with opacity */
|
||||||
|
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.39 0.0215 96.47 / 80%); /* black olive with opacity */
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.39 0.0215 96.47); /* black olive */
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.39 0.0215 96.47 / 70%); /* black olive with opacity */
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(0.97 0.0228 95.96 / 20%); /* cosmic latte with opacity */
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.33 0.1316 336.24); /* palatinate */
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.67 0.0763 198.81 / 70%); /* verdigris with opacity */
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.39 0.0215 96.47 / 90%); /* black olive with opacity */
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.49 0.1326 259.29); /* denim */
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.97 0.0228 95.96); /* cosmic latte */
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
0.97 0.0228 95.96 / 10%
|
||||||
|
); /* cosmic latte with opacity */
|
||||||
|
--sidebar-ring: oklch(0.67 0.0763 198.81); /* verdigris */
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user