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:
13
app/analytics.tsx
Normal file
13
app/analytics.tsx
Normal 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;
|
139
app/globals.css
139
app/globals.css
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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){}})()",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
12
components/layout/theme-script.tsx
Normal file
12
components/layout/theme-script.tsx
Normal 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){}})()",
|
||||
}}
|
||||
/>
|
||||
);
|
@ -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}
|
||||
|
Reference in New Issue
Block a user