1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:25:22 -04:00

initial tailwind conversion

This commit is contained in:
Jake Jarvis 2025-04-24 10:42:16 -04:00
parent f277119407
commit c9a8b3eaa5
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
40 changed files with 785 additions and 1543 deletions

View File

@ -16,15 +16,18 @@
"git.fetchOnPull": true,
"git.rebaseWhenSync": true,
"telemetry.telemetryLevel": "off",
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.surveys.enabled": false,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsserver.log": "off",
"typescript.updateImportsOnFileMove.enabled": "always"
},
"extensions": [
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"unifiedjs.vscode-mdx",
"esbenp.prettier-vscode",
"stylelint.vscode-stylelint"
"unifiedjs.vscode-mdx"
]
}
},

1
.npmrc
View File

@ -1,3 +1,2 @@
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*stylelint*

View File

@ -1,12 +1,13 @@
/** @type {import("prettier").Config} */
const config = {
singleQuote: false,
plugins: ["prettier-plugin-tailwindcss"],
jsxSingleQuote: false,
printWidth: 120,
tabWidth: 2,
useTabs: false,
quoteProps: "as-needed",
singleQuote: false,
tabWidth: 2,
trailingComma: "es5",
useTabs: false,
};
export default config;

View File

@ -1,19 +0,0 @@
/* eslint-disable import/no-anonymous-default-export */
/** @type {import("stylelint").Config} */
export default {
extends: ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-prettier/recommended"],
rules: {
"selector-class-pattern": null,
"custom-property-pattern": null,
"media-feature-range-notation": null,
"rule-empty-line-before": [
"always-multi-line",
{
except: ["after-single-line-comment"],
ignore: ["inside-block"],
},
],
"color-hex-length": "long",
},
};

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"unifiedjs.vscode-mdx"
]
}

16
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"editor.tabSize": 2,
"editor.rulers": [
120
],
"files.associations": {
"*.css": "tailwindcss",
"*.mdx": "markdown"
},
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"typescript.surveys.enabled": false,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsserver.log": "off",
"typescript.updateImportsOnFileMove.enabled": "always"
}

View File

@ -97,7 +97,7 @@ const ContactForm = () => {
Markdown syntax
</Link>{" "}
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
<Link href="https://jarv.is" plain>
<Link href="https://jarv.is" className="hover:no-underline">
links
</Link>
](https://jarv.is), and <code>`code`</code>.

View File

@ -11,7 +11,7 @@ export const GeistSans = GeistSansLoader({
"system-ui",
"sans-serif",
],
variable: "--fonts-sans",
variable: "--font-geist-sans",
preload: true,
});
@ -29,6 +29,6 @@ export const GeistMono = GeistMonoLoader({
"monospace",
],
adjustFontFallback: false,
variable: "--fonts-mono",
variable: "--font-geist-mono",
preload: true,
});

View File

@ -1,69 +1,100 @@
/*!
* modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize/tree/v3.0.1
*/
@import "tailwindcss";
*,
::before,
::after {
box-sizing: border-box;
@theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
html {
line-height: 1.15;
tab-size: 4;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-text-size-adjust: 100%;
@theme {
--container-default: var(--container-4xl);
--color-*: initial;
--color-background-inner: #ffffff;
--color-background-outer: #fcfcfc;
--color-text: #202020;
--color-medium-dark: #515151;
--color-medium: #5e5e5e;
--color-medium-light: #757575;
--color-light: #d2d2d2;
--color-kinda-light: #e3e3e3;
--color-super-light: #f4f4f4;
--color-super-duper-light: #fbfbfb;
--color-link: #0e6dc2;
--color-success: #44a248;
--color-error: #ff1b1b;
--color-warning: #f78200;
--animate-wave: wave 5s ease 1s infinite;
--animate-heartbeat: heartbeat 10s ease 7.5s infinite;
@keyframes wave {
0% {
transform: rotate(0deg);
}
5% {
transform: rotate(14deg);
}
10% {
transform: rotate(-8deg);
}
15% {
transform: rotate(14deg);
}
20% {
transform: rotate(-4deg);
}
25% {
transform: rotate(10deg);
}
30% {
transform: rotate(0deg);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: rotate(0deg);
}
}
@keyframes heartbeat {
0% {
transform: scale(1);
}
2% {
transform: scale(1.25);
}
4% {
transform: scale(1);
}
6% {
transform: scale(1.2);
}
8% {
transform: scale(1);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: scale(1);
}
}
}
body {
margin: 0;
font-family: var(--fonts-sans);
background-color: var(--colors-background-inner);
}
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
code,
kbd,
samp,
pre {
font-size: 1em;
font-family: var(--fonts-mono);
font-variant-ligatures: none; /* i hate them. fwiw. */
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
button,
input,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-appearance: button;
[data-theme="dark"] {
--color-background-inner: #1e1e1e;
--color-background-outer: #252525;
--color-text: #f1f1f1;
--color-medium-dark: #d7d7d7;
--color-medium: #b1b1b1;
--color-medium-light: #959595;
--color-light: #646464;
--color-kinda-light: #535353;
--color-super-light: #272727;
--color-super-duper-light: #1f1f1f;
--color-link: #88c7ff;
--color-success: #78df55;
--color-error: #ff5151;
--color-warning: #f2b702;
}

View File

@ -1,26 +0,0 @@
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.default {
width: 100%;
padding: 1.5em;
font-size: 0.9em;
line-height: 1.7;
color: var(--colors-text);
}
.container {
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
}
@media (max-width: 768px) {
.default {
font-size: 0.925em;
line-height: 1.85;
}
}

View File

@ -1,27 +1,27 @@
import { env } from "../lib/env";
import { JsonLd } from "react-schemaorg";
import { Analytics } from "@vercel/analytics/next";
import clsx from "clsx";
import { ThemeProvider, ThemeScript } from "../contexts/ThemeContext";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
import cn from "../lib/helpers/classnames";
import { defaultMetadata } from "../lib/helpers/metadata";
import * as config from "../lib/config";
import { MAX_WIDTH } from "../lib/config/constants";
import type { Person, WebSite } from "schema-dts";
import { GeistMono, GeistSans } from "./fonts";
import "./globals.css";
import "./themes.css";
import styles from "./layout.module.css";
export const metadata = defaultMetadata;
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<html lang={env.NEXT_PUBLIC_SITE_LOCALE} suppressHydrationWarning>
<html
lang={env.NEXT_PUBLIC_SITE_LOCALE}
className={cn(GeistSans.variable, GeistMono.variable)}
suppressHydrationWarning
>
<head>
<ThemeScript />
@ -62,21 +62,16 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
/>
</head>
<body
className={clsx(GeistSans.variable, GeistMono.variable)}
style={{ ["--max-width" as string]: `${MAX_WIDTH}px` }}
>
<body className="bg-background-outer text-text font-sans">
<ThemeProvider>
<SkipNavLink />
<div className={styles.layout}>
<div className="mx-auto flex min-h-screen flex-col">
<Header />
<main className={styles.default}>
<div className={styles.container}>
<SkipNavTarget />
{children}
</div>
<main className="bg-background-inner w-full text-sm">
<SkipNavTarget />
<div className="max-w-default mx-auto p-5">{children}</div>
</main>
<Footer />

View File

@ -88,7 +88,10 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
/>
<div className={styles.meta}>
<Link href={`/${POSTS_DIR}/${frontmatter!.slug}`} plain className={clsx(styles.metaItem, styles.metaLink)}>
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
>
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
<Time date={frontmatter!.date} format="MMMM d, y" />
</Link>
@ -109,8 +112,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
title={`Edit "${frontmatter!.title}" on GitHub`}
plain
className={clsx(styles.metaItem, styles.metaLink)}
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
>
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
<span>Improve This Post</span>
@ -139,8 +141,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
plain
className={styles.link}
className={clsx("hover:no-underline", styles.link)}
/>
</h1>

View File

@ -1,84 +0,0 @@
.index h1 {
margin: 0 0 0.5em -1px; /* misaligned left margin, super nitpicky */
font-size: 1.925em;
font-weight: 500;
line-height: 1.2;
}
.index h2 {
margin: 0.5em 0 0.5em -1px;
font-size: 1.3em;
font-weight: 400;
line-height: 1.5;
}
.index p {
margin: 0.85em 0;
font-size: 1.05em;
line-height: 1.7;
}
.index p:last-of-type {
margin-bottom: 0;
}
.index sup {
margin: 0 0.1em;
font-size: 0.6em;
}
.wave {
margin-left: 0.1em;
font-size: 1.2em;
}
@media (prefers-reduced-motion: no-preference) {
.wave {
animation: wave 5s ease 1s infinite;
transform-origin: 65% 80%;
}
@keyframes wave {
0% {
transform: rotate(0deg);
}
5% {
transform: rotate(14deg);
}
10% {
transform: rotate(-8deg);
}
15% {
transform: rotate(14deg);
}
20% {
transform: rotate(-4deg);
}
25% {
transform: rotate(10deg);
}
30% {
transform: rotate(0deg);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: rotate(0deg);
}
}
}
@media (max-width: 768px) {
.index h1 {
font-size: 1.8em;
}
.index h2 {
font-size: 1.3em;
}
.index p {
font-size: 1em;
line-height: 1.9;
}
}

View File

@ -1,105 +1,58 @@
import clsx from "clsx";
import hash from "@emotion/hash";
import { rgba } from "polished";
import Link from "../components/Link";
import cn from "../lib/helpers/classnames";
import { LockIcon } from "lucide-react";
import UnstyledLink from "../components/Link";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./page.module.css";
const Link = ({
lightColor,
darkColor,
className,
children,
...rest
}: ComponentPropsWithoutRef<typeof UnstyledLink> & {
lightColor?: string;
darkColor?: string;
}) => {
if (lightColor && darkColor) {
const uniqueId = hash(`${lightColor},${darkColor}`);
return (
<UnstyledLink className={clsx(`t_${uniqueId}`, className)} {...rest}>
{children}
<style
// workaround to have react combine all of these inline styles into a single <style> tag up top, see:
// https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet
href={uniqueId}
precedence={styles.index}
>
{`.t_${uniqueId}{--colors-link:${lightColor};--colors-link-underline:${rgba(lightColor, 0.4)}}[data-theme="dark"] .t_${uniqueId}{--colors-link:${darkColor};--colors-link-underline:${rgba(darkColor, 0.4)}}`}
</style>
</UnstyledLink>
);
}
return (
<UnstyledLink className={className} {...rest}>
{children}
</UnstyledLink>
);
};
const Page = () => {
return (
<div className={styles.index}>
<h1>
Hi there! I&rsquo;m Jake. <span className={styles.wave}>👋</span>
<>
<h1 className="mb-2 text-3xl leading-5 font-medium">
Hi there! I&rsquo;m Jake. <span className="animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
</h1>
<h2>
<h2 className="my-4 text-xl leading-6 font-normal">
I&rsquo;m a frontend web developer based in the{" "}
<Link
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&amp;fs=1&amp;showinfo=1&amp;rel=0&amp;iv_load_policy=3"
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
lightColor="#fb4d42"
darkColor="#ff5146"
className={cn(`[--color-link:#fb4d42]`, `dark:[--color-link:#ff5146]`)}
>
Boston
</Link>{" "}
area.
</h2>
<p>
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
I specialize in using{" "}
<Link
href="https://www.typescriptlang.org/"
title="TypeScript Official Website"
lightColor="#235a97"
darkColor="#59a8ff"
className={cn(`[--color-link:#235a97]`, `dark:[--color-link:#59a8ff]`)}
>
TypeScript
</Link>
,{" "}
<Link href="https://reactjs.org/" title="React Official Website" lightColor="#1091b3" darkColor="#6fcbe3">
<Link href="https://reactjs.org/" className={cn(`[--color-link:#1091b3]`, `dark:[--color-link:#6fcbe3]`)}>
React
</Link>
, and{" "}
<Link href="https://nextjs.org/" title="Next.js Official Website" lightColor="#5e7693" darkColor="#a8b9c0">
<Link href="https://nextjs.org/" className={cn(`[--color-link:#5e7693]`, `dark:[--color-link:#a8b9c0]`)}>
Next.js
</Link>{" "}
to make lightweight{" "}
<Link
href="https://jamstack.org/glossary/jamstack/"
title="Jamstack Glossary"
lightColor="#04a699"
darkColor="#08bbac"
className={cn(`[--color-link:#04a699]`, `dark:[--color-link:#08bbac]`)}
>
Jamstack sites
</Link>{" "}
with dynamic and powerful{" "}
<Link href="https://nodejs.org/en/" title="Node.js Official Website" lightColor="#6fbc4e" darkColor="#84d95f">
<Link href="https://nodejs.org/en/" className={cn(`[--color-link:#6fbc4e]`, `dark:[--color-link:#84d95f]`)}>
Node
</Link>{" "}
backends. But I still know my way around{" "}
<Link
href="https://www.jetbrains.com/lp/php-25/"
title="25 Years of PHP History"
lightColor="#8892bf"
darkColor="#a4afe3"
className={cn(`[--color-link:#8892bf]`, `dark:[--color-link:#a4afe3]`)}
>
less buzzwordy
</Link>{" "}
@ -107,21 +60,19 @@ const Page = () => {
<Link
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
lightColor="#f48024"
darkColor="#e18431"
className={cn(`[--color-link:#f48024]`, `dark:[--color-link:#e18431]`)}
>
vanilla JavaScript
</Link>
), too.
</p>
<p>
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
Whenever possible, I also apply my experience in{" "}
<Link
href="https://bugcrowd.com/jakejarvis"
title="Jake Jarvis on Bugcrowd"
lightColor="#00b81a"
darkColor="#57f06d"
className={cn(`[--color-link:#00b81a]`, `dark:[--color-link:#57f06d]`)}
>
application security
</Link>
@ -129,8 +80,7 @@ const Page = () => {
<Link
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare'
lightColor="#0098ec"
darkColor="#43b9fb"
className={cn(`[--color-link:#0098ec]`, `dark:[--color-link:#43b9fb]`)}
>
serverless stacks
</Link>
@ -138,21 +88,19 @@ const Page = () => {
<Link
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
title='My repositories tagged with "github-actions" on GitHub'
lightColor="#ff6200"
darkColor="#f46c16"
className={cn(`[--color-link:#ff6200]`, `dark:[--color-link:#f46c16]`)}
>
DevOps automation
</Link>
.
</p>
<p>
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
I fell in love with{" "}
<Link
href="/previously"
title="My Terrible, Horrible, No Good, Very Bad First Websites"
lightColor="#4169e1"
darkColor="#8ca9ff"
className={cn(`[--color-link:#4169e1]`, `dark:[--color-link:#8ca9ff]`)}
>
frontend web design
</Link>{" "}
@ -160,8 +108,7 @@ const Page = () => {
<Link
href="/notes/my-first-code"
title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc"
darkColor="#d588fb"
className={cn(`[--color-link:#9932cc]`, `dark:[--color-link:#d588fb]`)}
>
backend programming
</Link>{" "}
@ -169,86 +116,76 @@ const Page = () => {
<Link
href="/birthday"
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088"
darkColor="#fd40b1"
className={cn(`[--color-link:#e40088]`, `dark:[--color-link:#fd40b1]`)}
style={{
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
}}
>
the Tooth Fairy
</Link>
. <span style={{ color: "var(--colors-medium-light)" }}>I&rsquo;ve improved a bit since then, I think? 🤷</span>
. <span className="text-medium-light">I&rsquo;ve improved a bit since then, I think? 🤷</span>
</p>
<p>
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
Over the years, some of my side projects{" "}
<Link
href="/leo"
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
lightColor="#ff1b1b"
darkColor="#f06060"
className={cn(`[--color-link:#ff1b1b]`, `dark:[--color-link:#f06060]`)}
>
have
</Link>{" "}
<Link
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
lightColor="#f78200"
darkColor="#fd992a"
className={cn(`[--color-link:#f78200]`, `dark:[--color-link:#fd992a]`)}
>
been
</Link>{" "}
<Link
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
lightColor="#f2b702"
darkColor="#ffcc2e"
className={cn(`[--color-link:#f2b702]`, `dark:[--color-link:#ffcc2e]`)}
>
featured
</Link>{" "}
<Link
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money'
lightColor="#5ebd3e"
darkColor="#78df55"
className={cn(`[--color-link:#5ebd3e]`, `dark:[--color-link:#78df55]`)}
>
by
</Link>{" "}
<Link
href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
lightColor="#009cdf"
darkColor="#29bfff"
className={cn(`[--color-link:#009cdf]`, `dark:[--color-link:#29bfff]`)}
>
various
</Link>{" "}
<Link
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
lightColor="#3e49bb"
darkColor="#7b87ff"
className={cn(`[--color-link:#3e49bb]`, `dark:[--color-link:#7b87ff]`)}
>
media
</Link>{" "}
<Link
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
lightColor="#973999"
darkColor="#db60dd"
className={cn(`[--color-link:#973999]`, `dark:[--color-link:#db60dd]`)}
>
outlets
</Link>
.
</p>
<p>
<p className="text-base leading-7 md:text-[0.975rem]">
You can find my work on{" "}
<Link
href="https://github.com/jakejarvis"
rel="me"
title="Jake Jarvis on GitHub"
lightColor="#8d4eff"
darkColor="#a379f0"
className={cn(`[--color-link:#8d4eff]`, `dark:[--color-link:#a379f0]`)}
>
GitHub
</Link>{" "}
@ -256,44 +193,34 @@ const Page = () => {
<Link
href="https://www.linkedin.com/in/jakejarvis/"
rel="me"
title="Jake Jarvis on LinkedIn"
lightColor="#0073b1"
darkColor="#3b9dd2"
className={cn(`[--color-link:#0073b1]`, `dark:[--color-link:#3b9dd2]`)}
>
LinkedIn
</Link>
. I&rsquo;m always available to connect over{" "}
<Link href="/contact" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
<Link
href="/contact"
title="Send an email"
className={cn(`[--color-link:#de0c0c]`, `dark:[--color-link:#ff5050]`)}
>
email
</Link>{" "}
<sup>
<sup className="mx-0.5 text-[0.6rem]">
<Link
href="https://jrvs.io/pgp"
rel="pgpkey"
title="My Public Key"
lightColor="#757575"
darkColor="#959595"
plain
className={cn(`[--color-link:#757575]`, `dark:[--color-link:#959595]`, "hover:no-underline")}
>
<LockIcon size="1.25em" style={{ verticalAlign: "text-top" }} />{" "}
<code
style={{
margin: "0 0.15em",
letterSpacing: "0.075em",
wordSpacing: "-0.4em",
}}
>
2B0C 9CF2 51E6 9A39
</code>
<LockIcon size="1.25em" className="inline align-text-top" />{" "}
<code className="mx-0.5 tracking-wider [word-spacing:-4px]">2B0C 9CF2 51E6 9A39</code>
</Link>
</sup>
,{" "}
<Link
href="https://bsky.app/profile/jarv.is"
rel="me"
title="Jake Jarvis on Bluesky"
lightColor="#0085ff"
darkColor="#208bfe"
className={cn(`[--color-link:#0085ff]`, `dark:[--color-link:#208bfe]`)}
>
Bluesky
</Link>
@ -301,15 +228,13 @@ const Page = () => {
<Link
href="https://fediverse.jarv.is/@jake"
rel="me"
title="Jake Jarvis on Mastodon"
lightColor="#6d6eff"
darkColor="#7b87ff"
className={cn(`[--color-link:#6d6eff]`, `dark:[--color-link:#7b87ff]`)}
>
Mastodon
</Link>{" "}
as well!
</p>
</div>
</>
);
};

View File

@ -1,94 +0,0 @@
.heading {
font-size: 1.4em;
font-weight: 400;
}
.calendar {
--activity-0: #ebedf0;
--activity-1: #9be9a8;
--activity-2: #40c463;
--activity-3: #30a14e;
--activity-4: #216e39;
}
[data-theme="dark"] .calendar {
--activity-0: #252525;
--activity-1: #033a16;
--activity-2: #196c2e;
--activity-3: #2ea043;
--activity-4: #56d364;
}
.calendar :global(.react-activity-calendar) {
margin: 1em auto 2em;
}
.calendar :global(.react-activity-calendar__count),
.calendar :global(.react-activity-calendar__legend-month) {
color: var(--colors-medium);
}
.calendar :global(.react-activity-calendar__legend-colors) {
color: var(--colors-medium-light);
}
.grid {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
width: 100%;
line-height: 1.1;
gap: 1em;
}
.card {
flex-grow: 1;
width: 370px;
padding: 1.2em 1.2em 0.8em;
border: 1px solid var(--colors-kinda-light);
border-radius: 1em;
font-size: 0.9em;
color: var(--colors-medium-dark);
}
.card .name {
display: inline-block;
margin-bottom: 0.4em;
font-size: 1.2em;
font-weight: 600;
}
.card .description {
margin: 0;
line-height: 1.7;
}
.card .meta {
display: flex;
flex-wrap: wrap;
margin-top: 0.4em;
}
.card .metaItem {
margin: 0.3em 1.5em 0.3em 0;
color: var(--colors-medium);
white-space: nowrap;
}
.card .metaLink {
color: inherit;
}
.card .metaLink:hover,
.card .metaLink:focus-visible {
color: var(--colors-link);
}
.card .metaIcon {
display: inline-block;
width: 1.25em;
height: 1.25em;
margin-right: 0.5em;
vertical-align: text-top;
}

View File

@ -6,11 +6,10 @@ import Calendar from "./calendar";
import PageTitle from "../../components/PageTitle";
import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime";
import cn from "../../lib/helpers/classnames";
import { createMetadata } from "../../lib/helpers/metadata";
import { getContributions, getRepos } from "./github";
import styles from "./page.module.css";
export const metadata = createMetadata({
title: "Projects",
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
@ -32,41 +31,55 @@ const Page = async () => {
<>
<PageTitle canonical="/projects">Projects</PageTitle>
<h2 className={styles.heading}>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain>
<h2 className="my-3.5 text-[1.4em] font-normal">
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}
className="text-inherit hover:no-underline"
>
Contribution activity
</Link>
</h2>
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
<Calendar data={contributions} className={styles.calendar} />
<div
className={cn(
"mx-auto mt-4 mb-8",
"[--activity-0:#ebedf0] [--activity-1:#9be9a8] [--activity-2:#40c463] [--activity-3:#30a14e] [--activity-4:#216e39]",
"dark:[--activity-0:#252525] dark:[--activity-1:#033a16] dark:[--activity-2:#196c2e] dark:[--activity-3:#2ea043] dark:[--activity-4:#56d364]",
String.raw`[&_.react-activity-calendar\_\_count]:text-medium [&_.react-activity-calendar\_\_legend-month]:text-medium [&_.react-activity-calendar\_\_legend-colors]:text-medium-light`
)}
>
<Calendar data={contributions} />
</div>
</Suspense>
<h2 className={styles.heading}>
<h2 className="my-3.5 text-[1.4em] font-normal">
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
style={{ color: "inherit" }}
plain
className="text-inherit hover:no-underline"
>
Popular repositories
</Link>
</h2>
<div className={styles.grid}>
<div className="flex w-full flex-row flex-wrap items-start justify-between gap-[1em] leading-[1.1]">
{repos?.map((repo) => (
<div key={repo!.name} className={styles.card}>
<Link href={repo!.url} className={styles.name}>
<div
key={repo!.name}
className="border-kinda-light text-medium-dark w-[370px] grow rounded-[1em] border border-solid px-[1.2em] pt-[1.2em] pb-[0.8em] text-[0.9em]"
>
<Link href={repo!.url} className="mb-[0.4em] inline-block text-[1.2em] font-semibold">
{repo!.name}
</Link>
{repo!.description && <p className={styles.description}>{repo!.description}</p>}
{repo!.description && <p className="m-0 leading-[1.7]">{repo!.description}</p>}
<div className={styles.meta}>
<div className="mt-[0.4em] flex flex-wrap">
{repo!.primaryLanguage && (
<div className={styles.metaItem}>
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
{repo!.primaryLanguage.color && (
<span
className={styles.metaIcon}
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
/>
)}
@ -75,41 +88,38 @@ const Page = async () => {
)}
{repo!.stargazerCount > 0 && (
<div className={styles.metaItem}>
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<Link
href={`${repo!.url}/stargazers`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
plain
className={styles.metaLink}
className="hover:text-link text-inherit hover:no-underline"
>
<StarIcon size="1.25em" className={styles.metaIcon} />
<StarIcon size="1.25em" className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top" />
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
</Link>
</div>
)}
{repo!.forkCount > 0 && (
<div className={styles.metaItem}>
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<Link
href={`${repo!.url}/network/members`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
plain
className={styles.metaLink}
className="hover:text-link text-inherit hover:no-underline"
>
<GitForkIcon size="1.25em" className={styles.metaIcon} />
<GitForkIcon
size="1.25em"
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
/>
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
</Link>
</div>
)}
<div className={styles.metaItem}>
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<span
className={styles.metaIcon}
style={{
// invisible icon hack to fix line height
width: 0,
marginRight: 0,
}}
// invisible icon hack to fix line height
className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
/>
<span>
Updated <RelativeTime date={repo!.pushedAt} />
@ -120,13 +130,7 @@ const Page = async () => {
))}
</div>
<p
style={{
textAlign: "center",
marginBottom: 0,
fontWeight: 500,
}}
>
<p className="mt-4 mb-0 text-center text-base font-medium">
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
View more on{" "}
<svg
@ -134,13 +138,7 @@ const Page = async () => {
viewBox="0 0 24 24"
width="1.2em"
height="1.2em"
style={{
width: "1.2em",
height: "1.2em",
verticalAlign: "text-top",
margin: "0 0.1em 0 0.25em",
fill: "var(--colors-text)",
}}
className="fill-text mr-[0.1em] ml-[0.25em] inline h-[1.2em] w-[1.2em] align-text-top"
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>{" "}

View File

@ -45,7 +45,7 @@ const Page = () => {
>
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} plain>
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} className="hover:no-underline">
/stable_products_that_people_rely_on/
</Link>
googledomains.zip /tmp/
@ -64,7 +64,7 @@ const Page = () => {
<br />
<span style={{ color: "#78df55" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} plain>
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} className="hover:no-underline">
*.zip
</Link>
<br />

View File

@ -1,6 +0,0 @@
.blockquote {
margin-left: 0;
padding-left: 1.25em;
border-left: 0.25em solid var(--colors-link);
color: var(--colors-medium-dark);
}

View File

@ -1,12 +1,10 @@
import clsx from "clsx";
import cn from "../../lib/helpers/classnames";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Blockquote.module.css";
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
<blockquote className={clsx(styles.blockquote, className)} {...rest} />
<blockquote className={cn("border-l-link text-medium-dark ml-0 border-l-4 pl-5", className)} {...rest} />
);
export default Blockquote;

View File

@ -1,88 +0,0 @@
.footer {
flex: 1;
width: 100%;
padding: 1.25em 1.5em;
border-top: 1px solid var(--colors-kinda-light);
background-color: var(--colors-background-outer);
color: var(--colors-medium-dark);
}
.row {
display: flex;
justify-content: space-between;
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
font-size: 0.8em;
line-height: 2.3;
}
.link {
color: var(--colors-medium-dark) !important;
}
.link:has(.icon):hover,
.link:has(.icon):focus-visible {
color: var(--colors-medium) !important;
}
.link.underline {
padding-bottom: 2px;
border-bottom: 1px solid var(--colors-light);
}
.link.underline:hover,
.link.underline:focus-visible {
border-bottom-color: var(--colors-kinda-light);
}
.icon {
width: 1.25em;
height: 1.25em;
margin: 0 0.1em;
vertical-align: text-top;
}
.heart {
color: var(--colors-error);
}
@media (prefers-reduced-motion: no-preference) {
.heart {
animation: pulse 10s ease 7.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
2% {
transform: scale(1.25);
}
4% {
transform: scale(1);
}
6% {
transform: scale(1.2);
}
8% {
transform: scale(1);
}
/* pause for ~9 out of 10 seconds */
100% {
transform: scale(1);
}
}
}
@media (max-width: 768px) {
.footer {
padding: 1em 1.25em;
}
/* stack columns on left instead of flexboxing across */
.row {
display: block;
}
}

View File

@ -1,38 +1,44 @@
import { env } from "../../lib/env";
import clsx from "clsx";
import { HeartIcon } from "lucide-react";
import Link from "../Link";
import cn from "../../lib/helpers/classnames";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Footer.module.css";
export type FooterProps = ComponentPropsWithoutRef<"footer">;
const Footer = ({ className, ...rest }: FooterProps) => {
return (
<footer className={clsx(styles.footer, className)} {...rest}>
<div className={styles.row}>
<footer
className={cn("border-t-kinda-light bg-background-outer text-medium-dark w-full border-t py-4", className)}
{...rest}
>
<div className="max-w-default mx-auto flex w-full flex-col justify-between px-5 text-[0.8rem] leading-9 md:flex-row">
<div>
Content{" "}
<Link href="/license" title={config.license} plain className={styles.link}>
<Link href="/license" title={config.license} className="text-medium-dark hover:no-underline">
licensed under {config.licenseAbbr}
</Link>
,{" "}
<Link href="/previously" title="Previously on..." plain className={styles.link}>
<Link href="/previously" title="Previously on..." className="text-medium-dark hover:no-underline">
{config.copyrightYearStart}
</Link>{" "}
{new Date().getUTCFullYear()}.
</div>
<div>
Made with <HeartIcon size="1.25em" fill="currentColor" className={clsx(styles.icon, styles.heart)} /> and{" "}
Made with{" "}
<HeartIcon
size="1.25em"
fill="currentColor"
className="animate-heartbeat text-error mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
/>{" "}
and{" "}
<Link
href="https://nextjs.org/"
title="Powered by Next.js"
aria-label="Next.js"
plain
className={styles.link}
className="text-medium-dark hover:text-medium hover:no-underline"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -42,7 +48,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
viewBox="0 0 24 24"
width="1.25em"
height="1.25em"
className={styles.icon}
className="mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
>
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
</svg>
@ -51,8 +57,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
<Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
title="View Source on GitHub"
plain
className={clsx(styles.link, styles.underline)}
className="border-b-light text-medium-dark hover:border-b-kinda-light border-b-2 pb-0.5 hover:no-underline"
>
View source.
</Link>

View File

@ -1,83 +0,0 @@
.header {
width: 100%;
height: 4.5em;
padding: 0.7em 1.5em;
border-bottom: 1px solid var(--colors-kinda-light);
background-color: var(--colors-background-header);
/* make sticky */
position: sticky;
top: 0;
z-index: 1000;
/* blurry glass-like background effect (except on firefox...?) */
backdrop-filter: saturate(180%) blur(5px);
}
.avatar {
width: 50px;
height: 50px;
border: 1px solid var(--colors-light);
border-radius: 50%;
}
.home {
display: flex;
flex-shrink: 0;
align-items: center;
color: var(--colors-medium-dark) !important;
}
.home:hover,
.home:focus-visible {
color: var(--colors-link) !important;
}
.name {
margin: 0 0.6em;
font-size: 1.15em;
font-weight: 500;
letter-spacing: 0.02em;
line-height: 1;
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
}
@media (max-width: 768px) {
.header {
padding: 0.75em 1.25em;
height: 5.9em;
}
.avatar {
width: 70px;
height: 70px;
border-width: 2px;
}
.home:hover .avatar,
.home:focus-visible .avatar {
border-color: var(--colors-link-underline);
}
.name {
display: none;
}
.menu {
max-width: 325px;
}
}
@media (max-width: 380px) {
.menu {
max-width: 225px;
}
}

View File

@ -1,34 +1,46 @@
import Image from "next/image";
import clsx from "clsx";
import Link from "../Link";
import Menu from "../Menu";
import cn from "../../lib/helpers/classnames";
import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Header.module.css";
import avatarImg from "../../app/avatar.jpg";
export type HeaderProps = ComponentPropsWithoutRef<"header">;
const Header = ({ className, ...rest }: HeaderProps) => {
return (
<header className={clsx(styles.header, className)} {...rest}>
<nav className={styles.nav}>
<Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}>
<header
className={cn(
"bg-background-outer/70 border-kinda-light sticky top-0 z-[1000] h-24 w-full border-b backdrop-blur-[5px] backdrop-saturate-[180%] md:h-18",
className
)}
{...rest}
>
<nav className="max-w-default mx-auto flex h-full w-full items-center justify-between px-5 py-3 md:py-5">
<Link
dynamicOnHover
href="/"
rel="author"
aria-label={config.authorName}
className="text-medium-dark hover:text-link flex flex-shrink-0 items-center hover:no-underline"
>
<Image
src={avatarImg}
alt={`Photo of ${config.authorName}`}
className={styles.avatar}
className="border-light h-[70px] w-[70px] rounded-full border-2 md:h-[48px] md:w-[48px] md:border"
width={70}
height={70}
quality={50}
priority
/>
<span className={styles.name}>{config.authorName}</span>
<span className="mx-2.5 hidden text-lg leading-none font-medium tracking-[0.02em] md:block">
{config.authorName}
</span>
</Link>
<Menu className={styles.menu} />
<Menu className="ml-6 w-full max-w-64 sm:ml-4 sm:max-w-96 md:ml-0 md:max-w-none" />
</nav>
</header>
);

View File

@ -8,7 +8,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
return (
<Link href={`#${id}`} plain {...rest}>
<Link href={`#${id}`} className="hover:no-underline" {...rest}>
<LinkIcon size="0.8em" />
</Link>
);

View File

@ -1,25 +0,0 @@
.link {
color: var(--colors-link);
text-decoration: none;
}
/* fancy underline effect on hover */
.link:not(.plain) {
background-image: linear-gradient(var(--colors-link-underline), var(--colors-link-underline));
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% 2px;
transition: background-size 0.2s ease-in-out;
padding-bottom: 3px;
}
.link:not(.plain):hover,
.link:not(.plain):focus-visible {
background-size: 100% 2px;
}
@media (prefers-reduced-motion: reduce) {
.link:not(.plain) {
transition: none !important;
}
}

View File

@ -1,18 +1,13 @@
import NextLink from "next/link";
import clsx from "clsx";
import cn from "../../lib/helpers/classnames";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css";
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
/** Disables fancy text-decoration effect when true. */
plain?: boolean;
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
dynamicOnHover?: boolean;
};
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, className, ...rest }: LinkProps) => {
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, className, ...rest }: LinkProps) => {
// This component auto-detects whether or not this link should open in the same window (the default for internal
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
@ -25,10 +20,8 @@ const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, clas
href={href}
target={target || (isExternal ? "_blank" : undefined)}
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
className={clsx(
styles.link,
// eslint-disable-next-line css-modules/no-undef-class
plain && styles.plain,
className={cn(
"text-link hover:decoration-link/40 hover:underline hover:decoration-2 hover:underline-offset-4",
className
)}
{...rest}

View File

@ -1,36 +0,0 @@
.menu {
display: flex;
flex-direction: row;
align-items: center;
padding: 0;
margin: 0;
}
.item {
display: block;
margin-left: 1em;
list-style: none;
}
@media (max-width: 768px) {
.menu {
width: 100%;
justify-content: space-between;
margin-left: 1em;
}
.item {
margin-left: 0;
}
}
@media (max-width: 380px) {
.menu {
margin-left: 1.4em;
}
/* the home icon is kinda redundant when space is SUPER tight */
.item:first-of-type {
display: none;
}
}

View File

@ -1,39 +1,31 @@
"use client";
import { usePathname } from "next/navigation";
import clsx from "clsx";
import MenuItem from "../MenuItem";
import ThemeToggle from "../ThemeToggle";
import cn from "../../lib/helpers/classnames";
import { menuItems } from "../../lib/config/menu";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./Menu.module.css";
export type MenuProps = ComponentPropsWithoutRef<"ul">;
const Menu = ({ className, ...rest }: MenuProps) => {
const pathname = usePathname() || "";
return (
<ul className={clsx(styles.menu, className)} {...rest}>
<ul className={cn("flex flex-row justify-between md:justify-end", className)} {...rest}>
{menuItems.map((item, index) => {
// kinda weird/hacky way to determine if the *first part* of the current path matches this href
const isCurrent = item.href === `/${pathname.split("/")[1]}`;
return (
<li className={styles.item} key={item.text || index}>
<li className="first-of-type:hidden sm:first-of-type:block md:ml-4" key={item.text || index}>
<MenuItem {...item} current={isCurrent} />
</li>
);
})}
<li
className={styles.item}
style={{
// manually align the theme toggle with the rest of the menu icons
paddingTop: "0.2em",
}}
>
<li className="-mr-2.5 md:ml-4">
<MenuItem
// @ts-expect-error
icon={ThemeToggle}

View File

@ -1,43 +0,0 @@
.link {
display: inline-flex;
align-items: center;
padding: 0.6em;
margin-top: 0.2em;
color: var(--colors-medium-dark) !important;
}
/* indicate active page/section */
.link.current {
margin-bottom: -0.2em;
border-bottom: 0.2em solid var(--colors-link-underline);
}
.link:not(.current):hover,
.link:not(.current):focus-visible {
margin-bottom: -0.2em;
border-bottom: 0.2em solid var(--colors-kinda-light);
}
.icon {
display: block;
width: 1.25em;
height: 1.25em;
}
.label {
margin-left: 0.7em;
font-size: 0.925em;
font-weight: 500;
letter-spacing: 0.025em;
}
@media (max-width: 768px) {
.icon {
width: 1.8em;
height: 1.8em;
}
.label {
display: none;
}
}

View File

@ -1,10 +1,8 @@
import clsx from "clsx";
import cn from "../../lib/helpers/classnames";
import Link from "../Link";
import type { ComponentPropsWithoutRef } from "react";
import type { LucideIcon } from "lucide-react";
import styles from "./MenuItem.module.css";
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
text?: string;
href?: string;
@ -17,8 +15,8 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
const item = (
<>
{Icon && <Icon size="1.25em" className={styles.icon} />}
{text && <span className={styles.label}>{text}</span>}
{Icon && <Icon size="1.25em" className="block h-[1.8em] w-[1.8em] md:h-[1.25em] md:w-[1.25em]" />}
{text && <span className="ml-3 hidden text-sm leading-none font-medium tracking-[0.02em] md:block">{text}</span>}
</>
);
@ -29,8 +27,12 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
dynamicOnHover
href={href}
aria-label={text}
plain
className={clsx(styles.link, current && styles.current, className)}
data-current={current || undefined}
className={cn(
"text-medium-dark hover:border-kinda-light -mb-[0.2em] inline-flex items-center p-2.5 hover:border-b-[0.2em] hover:no-underline",
current && "border-link/40 hover:border-link/40 border-b-[0.2em]",
className
)}
{...rest}
>
{item}

View File

@ -1,15 +0,0 @@
.title {
margin-top: 0;
margin-bottom: 0.6em;
font-size: 2em;
font-weight: 500;
text-align: left;
text-transform: lowercase;
}
.slug::before {
content: "\002E\002F"; /* "./" */
letter-spacing: 0.1em;
color: var(--colors-medium-light);
margin-right: -0.1em;
}

View File

@ -1,17 +1,18 @@
import clsx from "clsx";
import cn from "../../lib/helpers/classnames";
import Link from "../Link";
import type { ComponentPropsWithoutRef } from "react";
import styles from "./PageTitle.module.css";
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
canonical: string;
};
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
return (
<h1 className={clsx(styles.title, className)} {...rest}>
<Link href={canonical} plain className={styles.slug}>
<h1 className={cn("mt-1 mb-6 text-left text-3xl font-medium lowercase", className)} {...rest}>
<Link
href={canonical}
className="before:text-medium-light before:mr-[-0.1em] before:tracking-widest before:content-['\002E\002F']"
>
{children}
</Link>
</h1>

View File

@ -1,26 +0,0 @@
.toggle {
display: block;
border: 0;
padding: 0.6em;
margin-right: -0.6em;
background: none;
cursor: pointer;
color: var(--colors-medium-dark);
}
.toggle:hover,
.toggle:focus-visible {
color: var(--colors-warning);
}
/* hacky way to avoid flashing icon for a few milliseconds on initial render */
.toggle > .sun,
[data-theme="dark"] .toggle > .moon {
display: inherit;
}
/* stylelint-disable-next-line no-descending-specificity */
.toggle > .moon,
[data-theme="dark"] .toggle > .sun {
display: none;
}

View File

@ -6,8 +6,6 @@ import { useTheme } from "../../hooks";
import type { ComponentPropsWithoutRef } from "react";
import type { LucideIcon } from "lucide-react";
import styles from "./ThemeToggle.module.css";
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
@ -17,10 +15,10 @@ const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
aria-label="Toggle Theme"
className={styles.toggle}
className="hover:text-warning block bg-none p-2.5 hover:cursor-pointer hover:border-none"
>
<SunIcon className={clsx(styles.sun, className)} {...rest} />
<MoonIcon className={clsx(styles.moon, className)} {...rest} />
<SunIcon className={clsx("!block dark:!hidden", className)} {...rest} />
<MoonIcon className={clsx("!hidden dark:!block", className)} {...rest} />
</button>
);
};

View File

@ -2,4 +2,4 @@
export const POSTS_DIR = "notes";
/** Maximum width of content wrapper (e.g. for images) in pixels. */
export const MAX_WIDTH = 865;
export const MAX_WIDTH = 896;

View File

@ -0,0 +1,8 @@
import { twMerge } from "tailwind-merge";
import clsx, { type ClassValue } from "clsx";
const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};
export default cn;

View File

@ -20,7 +20,6 @@
"dependencies": {
"@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0",
"@emotion/hash": "^0.9.2",
"@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
@ -41,7 +40,6 @@
"html-entities": "^2.6.0",
"lucide-react": "0.503.0",
"next": "15.4.0-canary.5",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"react": "19.1.0",
"react-activity-calendar": "^2.7.10",
@ -69,6 +67,8 @@
"resend": "^4.4.0",
"server-only": "0.0.1",
"shiki": "^3.3.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"unified": "^11.0.5",
"valibot": "^1.0.0"
},
@ -76,6 +76,7 @@
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.25.1",
"@jakejarvis/eslint-config": "^4.0.7",
"@tailwindcss/postcss": "^4.1.4",
"@types/mdx": "^2.0.13",
"@types/node": "^22.14.1",
"@types/prop-types": "^15.7.14",
@ -97,12 +98,10 @@
"eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"schema-dts": "^1.1.5",
"stylelint": "^16.19.0",
"stylelint-config-css-modules": "^4.4.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-prettier": "^5.0.3",
"typescript": "5.8.3"
},
"optionalDependencies": {
@ -119,9 +118,6 @@
"lint-staged": {
"*.{js,jsx,ts,tsx,md,mdx}": [
"eslint"
],
"*.css": [
"stylelint"
]
}
}

1108
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@ -23,6 +23,10 @@
"groupName": "react",
"rangeStrategy": "pin"
},
{
"matchPackageNames": ["tailwindcss", "@tailwindcss/*", "tailwind-merge", "prettier-plugin-tailwindcss"],
"groupName": "tailwindcss"
},
{
"matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"],
"groupName": "mdx"
@ -32,8 +36,8 @@
"groupName": "eslint"
},
{
"matchPackageNames": ["stylelint", "stylelint-*"],
"groupName": "stylelint"
"matchPackageNames": ["prettier", "prettier-*", "!prettier-plugin-tailwindcss"],
"groupName": "prettier"
},
{
"matchPackageNames": ["typescript"],