1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-27 17:25:43 -04:00

fix flash of white in dark mode

This commit is contained in:
2025-05-21 16:56:54 -04:00
parent 83f1cc2fa9
commit 879c2b9dbe
12 changed files with 128 additions and 118 deletions

13
app/analytics.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Analytics as VercelAnalytics } from "@vercel/analytics/next";
import { SpeedInsights as VercelSpeedInsights } from "@vercel/speed-insights/next";
const Analytics = () => {
return (
<>
<VercelAnalytics />
<VercelSpeedInsights />
</>
);
};
export default Analytics;

View File

@ -3,53 +3,61 @@
@custom-variant dark (&:where([data-theme=dark] *));
@layer base {
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.5 0.134 242.749);
--primary-foreground: oklch(0.97 0.014 254.604);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--warning: oklch(0.67 0.179 58.318);
--success: oklch(0.63 0.194 149.214);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
}
:root {
--background: oklch(1.00 0 0);
--foreground: oklch(0.26 0 0);
--card: oklch(1.00 0 0);
--card-foreground: oklch(0.26 0 0);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.26 0 0);
--primary: oklch(0.50 0.13 245.46);
--primary-foreground: oklch(0.99 0 0);
--secondary: oklch(0.98 0 0);
--secondary-foreground: oklch(0.33 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.98 0 0);
--accent-foreground: oklch(0.33 0 0);
--destructive: oklch(0.62 0.21 25.77);
--warning: oklch(0.67 0.179 58.318);
--success: oklch(0.63 0.194 149.214);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
[data-theme="dark"] {
--background: oklch(0.205 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.81 0.105 251.813);
--primary-foreground: oklch(0.18 0.0374 265.522);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--warning: oklch(0.8 0.184 86.047);
--success: oklch(0.79 0.209 151.711);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
}
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
[data-theme="dark"] {
--background: oklch(0.20 0 0);
--foreground: oklch(0.98 0 0);
--card: oklch(0.14 0.00 285.82);
--card-foreground: oklch(0.98 0 0);
--popover: oklch(0.14 0.00 285.82);
--popover-foreground: oklch(0.98 0 0);
--primary: oklch(0.81 0.10 251.81);
--primary-foreground: oklch(0.21 0.01 285.88);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.27 0.01 286.03);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.70 0.19 22.23);
--warning: oklch(0.8 0.184 86.047);
--success: oklch(0.79 0.209 151.711);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
@theme inline {
@ -58,11 +66,6 @@
--font-mono: var(--font-geist-mono);
--font-mono--font-feature-settings: "liga" 0;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@ -83,13 +86,26 @@
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
@theme {
--animate-wave: wave 5s ease 1s infinite;
--animate-heartbeat: heartbeat 10s ease 7.5s infinite;
--animate-loading: loading 1.5s infinite ease-in-out both;
--animate-marquee: marquee 30s infinite linear;
--animate-marquee: marquee 30s linear infinite;
@keyframes wave {
0%,
@ -129,17 +145,6 @@
}
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(0.6);
}
}
@keyframes marquee {
from {
transform: translateX(0);
@ -156,7 +161,7 @@
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground selection:bg-primary selection:text-primary-foreground;
}
::-webkit-scrollbar {
@ -166,12 +171,12 @@
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
background: color-mix(in oklab, var(--primary) 60%, transparent);
border-radius: 5px;
}
* {
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
scrollbar-color: color-mix(in oklab, var(--primary) 60%, transparent) transparent;
}
}

View File

@ -1,11 +1,11 @@
import { env } from "@/lib/env";
import { JsonLd } from "react-schemaorg";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { ThemeProvider, ThemeScript } from "@/components/layout/theme-context";
import { ThemeProvider } from "@/components/layout/theme-context";
import { ThemeScript } from "@/components/layout/theme-script";
import Header from "@/components/layout/header";
import Footer from "@/components/layout/footer";
import Toaster from "@/components/ui/sonner";
import Analytics from "@/app/analytics";
import { defaultMetadata } from "@/lib/metadata";
import { GeistMono, GeistSans } from "@/lib/fonts";
import siteConfig from "@/lib/config/site";
@ -77,7 +77,6 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
</ThemeProvider>
<Analytics />
<SpeedInsights />
</body>
</html>
);

View File

@ -55,7 +55,7 @@ const Page = async () => {
/>
{views > 0 && (
<span className="bg-muted text-muted-foreground inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
<span className="bg-muted text-foreground/65 inline-flex h-5 flex-nowrap items-center gap-1 rounded-md px-1.5 align-text-top text-xs font-semibold text-nowrap shadow select-none">
<EyeIcon className="inline-block size-4 shrink-0" />
<span className="inline-block leading-none">
{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(views)}

View File

@ -58,7 +58,7 @@ const Page = async () => {
<div className="row-auto grid w-full grid-cols-none gap-4 md:grid-cols-2">
{repos?.map((repo) => (
<div key={repo!.name} className="border-ring/65 h-fit space-y-1.5 rounded-2xl border-1 px-4 py-3 shadow-xs">
<div key={repo!.name} className="border-ring/30 h-fit space-y-1.5 rounded-2xl border-1 px-4 py-3 shadow-xs">
<Link href={repo!.url} className="inline-block text-base leading-relaxed font-semibold">
{repo!.name}
</Link>

View File

@ -1,22 +1,19 @@
"use client";
import { forwardRef, useState, useEffect } from "react";
import { useState, useEffect } from "react";
import copy from "copy-to-clipboard";
import { ClipboardIcon, CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
const CopyButton = (
{
source,
timeout = 2000,
className,
...rest
}: React.ComponentProps<"button"> & {
source: string;
timeout?: number;
},
ref: React.Ref<React.ComponentRef<"button">>
) => {
const CopyButton = ({
source,
timeout = 2000,
className,
...rest
}: React.ComponentProps<"button"> & {
source: string;
timeout?: number;
}) => {
const [copied, setCopied] = useState(false);
const handleCopy: React.MouseEventHandler<React.ComponentRef<"button">> = (e) => {
@ -48,7 +45,6 @@ const CopyButton = (
return (
<button
ref={ref}
onClick={handleCopy}
disabled={copied}
className={cn("cursor-pointer disabled:cursor-default", className)}
@ -60,4 +56,4 @@ const CopyButton = (
);
};
export default forwardRef(CopyButton);
export default CopyButton;

View File

@ -19,7 +19,7 @@ const Header = ({ className, ...rest }: React.ComponentProps<"header">) => {
<Image
src={avatarImg}
alt={`Photo of ${siteConfig.name}`}
className="border-ring/80 size-[64px] rounded-full border-2 md:size-[48px] md:border-1"
className="border-ring/40 size-[64px] rounded-full border-2 md:size-[48px] md:border-1"
width={64}
height={64}
quality={50}
@ -30,7 +30,7 @@ const Header = ({ className, ...rest }: React.ComponentProps<"header">) => {
</span>
</Link>
<Menu className="w-full max-w-64 sm:max-w-96 md:ml-0 md:max-w-none" />
<Menu className="w-full max-w-64 sm:max-w-96 md:max-w-none" />
</header>
);
};

View File

@ -1,4 +1,3 @@
import { isValidElement } from "react";
import Link from "@/components/link";
import { cn } from "@/lib/utils";
@ -9,15 +8,21 @@ const MenuItem = ({
current,
className,
...rest
}: Omit<React.ComponentProps<typeof Link>, "href"> & {
}: React.ComponentProps<"div"> & {
text?: string;
href?: `/${string}`;
icon?: React.ReactNode;
icon: React.ReactNode;
current?: boolean;
}) => {
const item = (
<div className="[&_svg]:stroke-foreground/85 inline-flex items-center [&_svg]:size-7 [&_svg]:md:size-5">
{isValidElement(icon) && icon}
<div
className={cn(
"[&_svg]:stroke-foreground/85 inline-flex items-center [&_svg]:size-7 [&_svg]:md:size-5",
className
)}
{...rest}
>
{icon}
{text && <span className="ml-3 text-sm leading-none font-medium tracking-wide max-md:sr-only">{text}</span>}
</div>
);
@ -30,11 +35,7 @@ const MenuItem = ({
href={href}
aria-label={text}
data-current={current || undefined}
className={cn(
"text-foreground/85 hover:border-ring data-current:border-primary/40! inline-flex items-center border-b-3 border-b-transparent hover:no-underline",
className
)}
{...rest}
className="text-foreground/85 hover:border-ring/80 data-current:border-primary/60! inline-flex items-center border-b-3 border-b-transparent hover:no-underline"
>
{item}
</Link>

View File

@ -44,10 +44,7 @@ const Menu = ({ className, ...rest }: React.ComponentProps<"div">) => {
const isCurrent = item.href?.split("/")[1] === segment;
return (
<div
className="inline-block border-t-3 border-t-transparent last:-mr-2.5 max-sm:first:hidden [&_a,&_button]:p-2.5"
key={index}
>
<div className="mt-[3px] inline-block last:-mr-2.5 max-sm:first:hidden **:[a,button]:p-2.5" key={index}>
<MenuItem {...item} current={isCurrent} />
</div>
);

View File

@ -51,16 +51,3 @@ export const ThemeProvider = ({ children }: React.PropsWithChildren) => {
return <ThemeContext.Provider value={providerValues}>{children}</ThemeContext.Provider>;
};
// loaded in <head> by layout.tsx to avoid blinding flash of unstyled content (FOUC). irrelevant after the first render
// since the theme provider above takes over.
// unminified JS: https://gist.github.com/jakejarvis/79b0ec8506bc843023546d0d29861bf0
export const ThemeScript = () => (
<script
id="restore-theme"
dangerouslySetInnerHTML={{
__html:
"(()=>{try{const e=document.documentElement,t='undefined'!=typeof Storage?window.localStorage.getItem('theme'):null,a=(t&&'dark'===t)??window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';e.dataset.theme=a,e.style.colorScheme=a}catch(e){}})()",
}}
/>
);

View File

@ -0,0 +1,12 @@
// loaded in <head> by layout.tsx to avoid blinding flash of unstyled content (FOUC). irrelevant after the first render
// when <ThemeProvider /> takes over.
// unminified JS: https://gist.github.com/jakejarvis/79b0ec8506bc843023546d0d29861bf0
export const ThemeScript = () => (
<script
id="restore-theme"
dangerouslySetInnerHTML={{
__html:
"(()=>{try{const e=document.documentElement,t='undefined'!=typeof Storage?window.localStorage.getItem('theme'):null,a=(t&&'dark'===t)??window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';e.dataset.theme=a,e.style.colorScheme=a}catch(e){}})()",
}}
/>
);

View File

@ -13,7 +13,7 @@ const ThemeToggle = ({ className, ...rest }: React.ComponentProps<"button">) =>
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
aria-label="Toggle theme"
className={cn(
"hover:*:stroke-warning block cursor-pointer bg-transparent not-dark:[&_.lucide-moon]:hidden dark:[&_.lucide-sun]:hidden",
"hover:[&_svg]:stroke-warning block cursor-pointer bg-transparent not-dark:[&_.lucide-moon]:hidden dark:[&_.lucide-sun]:hidden",
className
)}
{...rest}