1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 09:05: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.fetchOnPull": true,
"git.rebaseWhenSync": true, "git.rebaseWhenSync": true,
"telemetry.telemetryLevel": "off", "telemetry.telemetryLevel": "off",
"javascript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifierEnding": "minimal", "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": [ "extensions": [
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"unifiedjs.vscode-mdx",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"stylelint.vscode-stylelint" "unifiedjs.vscode-mdx"
] ]
} }
}, },

1
.npmrc
View File

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

View File

@ -1,12 +1,13 @@
/** @type {import("prettier").Config} */ /** @type {import("prettier").Config} */
const config = { const config = {
singleQuote: false, plugins: ["prettier-plugin-tailwindcss"],
jsxSingleQuote: false, jsxSingleQuote: false,
printWidth: 120, printWidth: 120,
tabWidth: 2,
useTabs: false,
quoteProps: "as-needed", quoteProps: "as-needed",
singleQuote: false,
tabWidth: 2,
trailingComma: "es5", trailingComma: "es5",
useTabs: false,
}; };
export default config; 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 Markdown syntax
</Link>{" "} </Link>{" "}
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [ 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 links
</Link> </Link>
](https://jarv.is), and <code>`code`</code>. ](https://jarv.is), and <code>`code`</code>.

View File

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

View File

@ -1,69 +1,100 @@
/*! @import "tailwindcss";
* modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize/tree/v3.0.1
*/
*, @theme inline {
::before, --font-sans: var(--font-geist-sans);
::after { --font-mono: var(--font-geist-mono);
box-sizing: border-box;
} }
html { @theme {
line-height: 1.15; --container-default: var(--container-4xl);
tab-size: 4;
/* stylelint-disable-next-line property-no-vendor-prefix */ --color-*: initial;
-webkit-text-size-adjust: 100%; --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 { @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
margin: 0;
font-family: var(--fonts-sans);
background-color: var(--colors-background-inner);
}
code, [data-theme="dark"] {
kbd, --color-background-inner: #1e1e1e;
samp, --color-background-outer: #252525;
pre { --color-text: #f1f1f1;
font-size: 1em; --color-medium-dark: #d7d7d7;
font-family: var(--fonts-mono); --color-medium: #b1b1b1;
font-variant-ligatures: none; /* i hate them. fwiw. */ --color-medium-light: #959595;
} --color-light: #646464;
--color-kinda-light: #535353;
small { --color-super-light: #272727;
font-size: 80%; --color-super-duper-light: #1f1f1f;
} --color-link: #88c7ff;
--color-success: #78df55;
sub, --color-error: #ff5151;
sup { --color-warning: #f2b702;
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;
} }

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

View File

@ -88,7 +88,10 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
/> />
<div className={styles.meta}> <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} /> <CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
<Time date={frontmatter!.date} format="MMMM d, y" /> <Time date={frontmatter!.date} format="MMMM d, y" />
</Link> </Link>
@ -109,8 +112,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<Link <Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`} href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
title={`Edit "${frontmatter!.title}" on GitHub`} title={`Edit "${frontmatter!.title}" on GitHub`}
plain className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
className={clsx(styles.metaItem, styles.metaLink)}
> >
<SquarePenIcon size="1.25em" className={styles.metaIcon} /> <SquarePenIcon size="1.25em" className={styles.metaIcon} />
<span>Improve This Post</span> <span>Improve This Post</span>
@ -139,8 +141,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
<Link <Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`} href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }} dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
plain className={clsx("hover:no-underline", styles.link)}
className={styles.link}
/> />
</h1> </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 Link from "../components/Link";
import hash from "@emotion/hash"; import cn from "../lib/helpers/classnames";
import { rgba } from "polished";
import { LockIcon } from "lucide-react"; 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 = () => { const Page = () => {
return ( return (
<div className={styles.index}> <>
<h1> <h1 className="mb-2 text-3xl leading-5 font-medium">
Hi there! I&rsquo;m Jake. <span className={styles.wave}>👋</span> Hi there! I&rsquo;m Jake. <span className="animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
</h1> </h1>
<h2> <h2 className="my-4 text-xl leading-6 font-normal">
I&rsquo;m a frontend web developer based in the{" "} I&rsquo;m a frontend web developer based in the{" "}
<Link <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" 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' title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
lightColor="#fb4d42" className={cn(`[--color-link:#fb4d42]`, `dark:[--color-link:#ff5146]`)}
darkColor="#ff5146"
> >
Boston Boston
</Link>{" "} </Link>{" "}
area. area.
</h2> </h2>
<p> <p className="my-3 text-base leading-7 md:text-[0.975rem]">
I specialize in using{" "} I specialize in using{" "}
<Link <Link
href="https://www.typescriptlang.org/" href="https://www.typescriptlang.org/"
title="TypeScript Official Website" className={cn(`[--color-link:#235a97]`, `dark:[--color-link:#59a8ff]`)}
lightColor="#235a97"
darkColor="#59a8ff"
> >
TypeScript TypeScript
</Link> </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 React
</Link> </Link>
, and{" "} , 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 Next.js
</Link>{" "} </Link>{" "}
to make lightweight{" "} to make lightweight{" "}
<Link <Link
href="https://jamstack.org/glossary/jamstack/" href="https://jamstack.org/glossary/jamstack/"
title="Jamstack Glossary" className={cn(`[--color-link:#04a699]`, `dark:[--color-link:#08bbac]`)}
lightColor="#04a699"
darkColor="#08bbac"
> >
Jamstack sites Jamstack sites
</Link>{" "} </Link>{" "}
with dynamic and powerful{" "} 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 Node
</Link>{" "} </Link>{" "}
backends. But I still know my way around{" "} backends. But I still know my way around{" "}
<Link <Link
href="https://www.jetbrains.com/lp/php-25/" href="https://www.jetbrains.com/lp/php-25/"
title="25 Years of PHP History" title="25 Years of PHP History"
lightColor="#8892bf" className={cn(`[--color-link:#8892bf]`, `dark:[--color-link:#a4afe3]`)}
darkColor="#a4afe3"
> >
less buzzwordy less buzzwordy
</Link>{" "} </Link>{" "}
@ -107,21 +60,19 @@ const Page = () => {
<Link <Link
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/" href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
title='"The Cost of Javascript Frameworks" by Tim Kadlec' title='"The Cost of Javascript Frameworks" by Tim Kadlec'
lightColor="#f48024" className={cn(`[--color-link:#f48024]`, `dark:[--color-link:#e18431]`)}
darkColor="#e18431"
> >
vanilla JavaScript vanilla JavaScript
</Link> </Link>
), too. ), too.
</p> </p>
<p> <p className="my-3 text-base leading-7 md:text-[0.975rem]">
Whenever possible, I also apply my experience in{" "} Whenever possible, I also apply my experience in{" "}
<Link <Link
href="https://bugcrowd.com/jakejarvis" href="https://bugcrowd.com/jakejarvis"
title="Jake Jarvis on Bugcrowd" title="Jake Jarvis on Bugcrowd"
lightColor="#00b81a" className={cn(`[--color-link:#00b81a]`, `dark:[--color-link:#57f06d]`)}
darkColor="#57f06d"
> >
application security application security
</Link> </Link>
@ -129,8 +80,7 @@ const Page = () => {
<Link <Link
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/" href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
title='"What is serverless computing?" on Cloudflare' title='"What is serverless computing?" on Cloudflare'
lightColor="#0098ec" className={cn(`[--color-link:#0098ec]`, `dark:[--color-link:#43b9fb]`)}
darkColor="#43b9fb"
> >
serverless stacks serverless stacks
</Link> </Link>
@ -138,21 +88,19 @@ const Page = () => {
<Link <Link
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers" href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
title='My repositories tagged with "github-actions" on GitHub' title='My repositories tagged with "github-actions" on GitHub'
lightColor="#ff6200" className={cn(`[--color-link:#ff6200]`, `dark:[--color-link:#f46c16]`)}
darkColor="#f46c16"
> >
DevOps automation DevOps automation
</Link> </Link>
. .
</p> </p>
<p> <p className="my-3 text-base leading-7 md:text-[0.975rem]">
I fell in love with{" "} I fell in love with{" "}
<Link <Link
href="/previously" href="/previously"
title="My Terrible, Horrible, No Good, Very Bad First Websites" title="My Terrible, Horrible, No Good, Very Bad First Websites"
lightColor="#4169e1" className={cn(`[--color-link:#4169e1]`, `dark:[--color-link:#8ca9ff]`)}
darkColor="#8ca9ff"
> >
frontend web design frontend web design
</Link>{" "} </Link>{" "}
@ -160,8 +108,7 @@ const Page = () => {
<Link <Link
href="/notes/my-first-code" href="/notes/my-first-code"
title="Jake's Bulletin Board, circa 2003" title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc" className={cn(`[--color-link:#9932cc]`, `dark:[--color-link:#d588fb]`)}
darkColor="#d588fb"
> >
backend programming backend programming
</Link>{" "} </Link>{" "}
@ -169,86 +116,76 @@ const Page = () => {
<Link <Link
href="/birthday" href="/birthday"
title="🎉 Cranky Birthday Boy on VHS Tape 📼" title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088" className={cn(`[--color-link:#e40088]`, `dark:[--color-link:#fd40b1]`)}
darkColor="#fd40b1"
style={{ 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`, 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 the Tooth Fairy
</Link> </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> <p className="my-3 text-base leading-7 md:text-[0.975rem]">
Over the years, some of my side projects{" "} Over the years, some of my side projects{" "}
<Link <Link
href="/leo" href="/leo"
title="Powncer segment on The Lab with Leo Laporte (G4techTV)" title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
lightColor="#ff1b1b" className={cn(`[--color-link:#ff1b1b]`, `dark:[--color-link:#f06060]`)}
darkColor="#f06060"
> >
have have
</Link>{" "} </Link>{" "}
<Link <Link
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/" href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily' title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
lightColor="#f78200" className={cn(`[--color-link:#f78200]`, `dark:[--color-link:#fd992a]`)}
darkColor="#fd992a"
> >
been been
</Link>{" "} </Link>{" "}
<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" 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)' title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
lightColor="#f2b702" className={cn(`[--color-link:#f2b702]`, `dark:[--color-link:#ffcc2e]`)}
darkColor="#ffcc2e"
> >
featured featured
</Link>{" "} </Link>{" "}
<Link <Link
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm" href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
title='"The new Facebook is on a roll" on CNN Money' title='"The new Facebook is on a roll" on CNN Money'
lightColor="#5ebd3e" className={cn(`[--color-link:#5ebd3e]`, `dark:[--color-link:#78df55]`)}
darkColor="#78df55"
> >
by by
</Link>{" "} </Link>{" "}
<Link <Link
href="https://www.wired.com/2007/04/our-web-servers/" href="https://www.wired.com/2007/04/our-web-servers/"
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired' title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
lightColor="#009cdf" className={cn(`[--color-link:#009cdf]`, `dark:[--color-link:#29bfff]`)}
darkColor="#29bfff"
> >
various various
</Link>{" "} </Link>{" "}
<Link <Link
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/" 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' title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
lightColor="#3e49bb" className={cn(`[--color-link:#3e49bb]`, `dark:[--color-link:#7b87ff]`)}
darkColor="#7b87ff"
> >
media media
</Link>{" "} </Link>{" "}
<Link <Link
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/" 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' title='"Your Next Client? The CEO&#39;s Son" on Advertising Age'
lightColor="#973999" className={cn(`[--color-link:#973999]`, `dark:[--color-link:#db60dd]`)}
darkColor="#db60dd"
> >
outlets outlets
</Link> </Link>
. .
</p> </p>
<p> <p className="text-base leading-7 md:text-[0.975rem]">
You can find my work on{" "} You can find my work on{" "}
<Link <Link
href="https://github.com/jakejarvis" href="https://github.com/jakejarvis"
rel="me" rel="me"
title="Jake Jarvis on GitHub" className={cn(`[--color-link:#8d4eff]`, `dark:[--color-link:#a379f0]`)}
lightColor="#8d4eff"
darkColor="#a379f0"
> >
GitHub GitHub
</Link>{" "} </Link>{" "}
@ -256,44 +193,34 @@ const Page = () => {
<Link <Link
href="https://www.linkedin.com/in/jakejarvis/" href="https://www.linkedin.com/in/jakejarvis/"
rel="me" rel="me"
title="Jake Jarvis on LinkedIn" className={cn(`[--color-link:#0073b1]`, `dark:[--color-link:#3b9dd2]`)}
lightColor="#0073b1"
darkColor="#3b9dd2"
> >
LinkedIn LinkedIn
</Link> </Link>
. I&rsquo;m always available to connect over{" "} . 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 email
</Link>{" "} </Link>{" "}
<sup> <sup className="mx-0.5 text-[0.6rem]">
<Link <Link
href="https://jrvs.io/pgp" href="https://jrvs.io/pgp"
rel="pgpkey" rel="pgpkey"
title="My Public Key" title="My Public Key"
lightColor="#757575" className={cn(`[--color-link:#757575]`, `dark:[--color-link:#959595]`, "hover:no-underline")}
darkColor="#959595"
plain
> >
<LockIcon size="1.25em" style={{ verticalAlign: "text-top" }} />{" "} <LockIcon size="1.25em" className="inline align-text-top" />{" "}
<code <code className="mx-0.5 tracking-wider [word-spacing:-4px]">2B0C 9CF2 51E6 9A39</code>
style={{
margin: "0 0.15em",
letterSpacing: "0.075em",
wordSpacing: "-0.4em",
}}
>
2B0C 9CF2 51E6 9A39
</code>
</Link> </Link>
</sup> </sup>
,{" "} ,{" "}
<Link <Link
href="https://bsky.app/profile/jarv.is" href="https://bsky.app/profile/jarv.is"
rel="me" rel="me"
title="Jake Jarvis on Bluesky" className={cn(`[--color-link:#0085ff]`, `dark:[--color-link:#208bfe]`)}
lightColor="#0085ff"
darkColor="#208bfe"
> >
Bluesky Bluesky
</Link> </Link>
@ -301,15 +228,13 @@ const Page = () => {
<Link <Link
href="https://fediverse.jarv.is/@jake" href="https://fediverse.jarv.is/@jake"
rel="me" rel="me"
title="Jake Jarvis on Mastodon" className={cn(`[--color-link:#6d6eff]`, `dark:[--color-link:#7b87ff]`)}
lightColor="#6d6eff"
darkColor="#7b87ff"
> >
Mastodon Mastodon
</Link>{" "} </Link>{" "}
as well! as well!
</p> </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 PageTitle from "../../components/PageTitle";
import Link from "../../components/Link"; import Link from "../../components/Link";
import RelativeTime from "../../components/RelativeTime"; import RelativeTime from "../../components/RelativeTime";
import cn from "../../lib/helpers/classnames";
import { createMetadata } from "../../lib/helpers/metadata"; import { createMetadata } from "../../lib/helpers/metadata";
import { getContributions, getRepos } from "./github"; import { getContributions, getRepos } from "./github";
import styles from "./page.module.css";
export const metadata = createMetadata({ export const metadata = createMetadata({
title: "Projects", title: "Projects",
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`, description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
@ -32,41 +31,55 @@ const Page = async () => {
<> <>
<PageTitle canonical="/projects">Projects</PageTitle> <PageTitle canonical="/projects">Projects</PageTitle>
<h2 className={styles.heading}> <h2 className="my-3.5 text-[1.4em] font-normal">
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain> <Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}
className="text-inherit hover:no-underline"
>
Contribution activity Contribution activity
</Link> </Link>
</h2> </h2>
<Suspense fallback={<p>Failed to generate activity calendar.</p>}> <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> </Suspense>
<h2 className={styles.heading}> <h2 className="my-3.5 text-[1.4em] font-normal">
<Link <Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`} href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
style={{ color: "inherit" }} className="text-inherit hover:no-underline"
plain
> >
Popular repositories Popular repositories
</Link> </Link>
</h2> </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) => ( {repos?.map((repo) => (
<div key={repo!.name} className={styles.card}> <div
<Link href={repo!.url} className={styles.name}> 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} {repo!.name}
</Link> </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 && ( {repo!.primaryLanguage && (
<div className={styles.metaItem}> <div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
{repo!.primaryLanguage.color && ( {repo!.primaryLanguage.color && (
<span <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%" }} style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
/> />
)} )}
@ -75,41 +88,38 @@ const Page = async () => {
)} )}
{repo!.stargazerCount > 0 && ( {repo!.stargazerCount > 0 && (
<div className={styles.metaItem}> <div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<Link <Link
href={`${repo!.url}/stargazers`} href={`${repo!.url}/stargazers`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`} title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
plain className="hover:text-link text-inherit hover:no-underline"
className={styles.metaLink}
> >
<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> <span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
</Link> </Link>
</div> </div>
)} )}
{repo!.forkCount > 0 && ( {repo!.forkCount > 0 && (
<div className={styles.metaItem}> <div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<Link <Link
href={`${repo!.url}/network/members`} href={`${repo!.url}/network/members`}
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`} title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
plain className="hover:text-link text-inherit hover:no-underline"
className={styles.metaLink}
> >
<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> <span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
</Link> </Link>
</div> </div>
)} )}
<div className={styles.metaItem}> <div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
<span <span
className={styles.metaIcon} // invisible icon hack to fix line height
style={{ className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
// invisible icon hack to fix line height
width: 0,
marginRight: 0,
}}
/> />
<span> <span>
Updated <RelativeTime date={repo!.pushedAt} /> Updated <RelativeTime date={repo!.pushedAt} />
@ -120,13 +130,7 @@ const Page = async () => {
))} ))}
</div> </div>
<p <p className="mt-4 mb-0 text-center text-base font-medium">
style={{
textAlign: "center",
marginBottom: 0,
fontWeight: 500,
}}
>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}> <Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
View more on{" "} View more on{" "}
<svg <svg
@ -134,13 +138,7 @@ const Page = async () => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="1.2em" width="1.2em"
height="1.2em" height="1.2em"
style={{ className="fill-text mr-[0.1em] ml-[0.25em] inline h-[1.2em] w-[1.2em] align-text-top"
width: "1.2em",
height: "1.2em",
verticalAlign: "text-top",
margin: "0 0.1em 0 0.25em",
fill: "var(--colors-text)",
}}
> >
<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" /> <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>{" "} </svg>{" "}

View File

@ -45,7 +45,7 @@ const Page = () => {
> >
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>: <span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root <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/ /stable_products_that_people_rely_on/
</Link> </Link>
googledomains.zip /tmp/ googledomains.zip /tmp/
@ -64,7 +64,7 @@ const Page = () => {
<br /> <br />
<span style={{ color: "#78df55" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp; <span style={{ color: "#78df55" }}>@monthly</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/ <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 *.zip
</Link> </Link>
<br /> <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 type { ComponentPropsWithoutRef } from "react";
import styles from "./Blockquote.module.css";
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">; export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
const Blockquote = ({ className, ...rest }: BlockquoteProps) => ( 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; 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 { env } from "../../lib/env";
import clsx from "clsx";
import { HeartIcon } from "lucide-react"; import { HeartIcon } from "lucide-react";
import Link from "../Link"; import Link from "../Link";
import cn from "../../lib/helpers/classnames";
import * as config from "../../lib/config"; import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Footer.module.css";
export type FooterProps = ComponentPropsWithoutRef<"footer">; export type FooterProps = ComponentPropsWithoutRef<"footer">;
const Footer = ({ className, ...rest }: FooterProps) => { const Footer = ({ className, ...rest }: FooterProps) => {
return ( return (
<footer className={clsx(styles.footer, className)} {...rest}> <footer
<div className={styles.row}> 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> <div>
Content{" "} 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} licensed under {config.licenseAbbr}
</Link> </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} {config.copyrightYearStart}
</Link>{" "} </Link>{" "}
{new Date().getUTCFullYear()}. {new Date().getUTCFullYear()}.
</div> </div>
<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 <Link
href="https://nextjs.org/" href="https://nextjs.org/"
title="Powered by Next.js" title="Powered by Next.js"
aria-label="Next.js" aria-label="Next.js"
plain className="text-medium-dark hover:text-medium hover:no-underline"
className={styles.link}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -42,7 +48,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="1.25em" width="1.25em"
height="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" /> <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> </svg>
@ -51,8 +57,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
<Link <Link
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`} href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
title="View Source on GitHub" title="View Source on GitHub"
plain className="border-b-light text-medium-dark hover:border-b-kinda-light border-b-2 pb-0.5 hover:no-underline"
className={clsx(styles.link, styles.underline)}
> >
View source. View source.
</Link> </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 Image from "next/image";
import clsx from "clsx";
import Link from "../Link"; import Link from "../Link";
import Menu from "../Menu"; import Menu from "../Menu";
import cn from "../../lib/helpers/classnames";
import * as config from "../../lib/config"; import * as config from "../../lib/config";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Header.module.css";
import avatarImg from "../../app/avatar.jpg"; import avatarImg from "../../app/avatar.jpg";
export type HeaderProps = ComponentPropsWithoutRef<"header">; export type HeaderProps = ComponentPropsWithoutRef<"header">;
const Header = ({ className, ...rest }: HeaderProps) => { const Header = ({ className, ...rest }: HeaderProps) => {
return ( return (
<header className={clsx(styles.header, className)} {...rest}> <header
<nav className={styles.nav}> className={cn(
<Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}> "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 <Image
src={avatarImg} src={avatarImg}
alt={`Photo of ${config.authorName}`} 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} width={70}
height={70} height={70}
quality={50} quality={50}
priority 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> </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> </nav>
</header> </header>
); );

View File

@ -8,7 +8,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => { const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
return ( return (
<Link href={`#${id}`} plain {...rest}> <Link href={`#${id}`} className="hover:no-underline" {...rest}>
<LinkIcon size="0.8em" /> <LinkIcon size="0.8em" />
</Link> </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 NextLink from "next/link";
import clsx from "clsx"; import cn from "../../lib/helpers/classnames";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Link.module.css";
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & { 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 // https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
dynamicOnHover?: boolean; 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 // 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"`. // 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]); const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
@ -25,10 +20,8 @@ const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, clas
href={href} href={href}
target={target || (isExternal ? "_blank" : undefined)} target={target || (isExternal ? "_blank" : undefined)}
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined} rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
className={clsx( className={cn(
styles.link, "text-link hover:decoration-link/40 hover:underline hover:decoration-2 hover:underline-offset-4",
// eslint-disable-next-line css-modules/no-undef-class
plain && styles.plain,
className className
)} )}
{...rest} {...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"; "use client";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import clsx from "clsx";
import MenuItem from "../MenuItem"; import MenuItem from "../MenuItem";
import ThemeToggle from "../ThemeToggle"; import ThemeToggle from "../ThemeToggle";
import cn from "../../lib/helpers/classnames";
import { menuItems } from "../../lib/config/menu"; import { menuItems } from "../../lib/config/menu";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./Menu.module.css";
export type MenuProps = ComponentPropsWithoutRef<"ul">; export type MenuProps = ComponentPropsWithoutRef<"ul">;
const Menu = ({ className, ...rest }: MenuProps) => { const Menu = ({ className, ...rest }: MenuProps) => {
const pathname = usePathname() || ""; const pathname = usePathname() || "";
return ( 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) => { {menuItems.map((item, index) => {
// kinda weird/hacky way to determine if the *first part* of the current path matches this href // kinda weird/hacky way to determine if the *first part* of the current path matches this href
const isCurrent = item.href === `/${pathname.split("/")[1]}`; const isCurrent = item.href === `/${pathname.split("/")[1]}`;
return ( 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} /> <MenuItem {...item} current={isCurrent} />
</li> </li>
); );
})} })}
<li <li className="-mr-2.5 md:ml-4">
className={styles.item}
style={{
// manually align the theme toggle with the rest of the menu icons
paddingTop: "0.2em",
}}
>
<MenuItem <MenuItem
// @ts-expect-error // @ts-expect-error
icon={ThemeToggle} 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 Link from "../Link";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import styles from "./MenuItem.module.css";
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & { export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
text?: string; text?: string;
href?: string; href?: string;
@ -17,8 +15,8 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
const item = ( const item = (
<> <>
{Icon && <Icon size="1.25em" className={styles.icon} />} {Icon && <Icon size="1.25em" className="block h-[1.8em] w-[1.8em] md:h-[1.25em] md:w-[1.25em]" />}
{text && <span className={styles.label}>{text}</span>} {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 dynamicOnHover
href={href} href={href}
aria-label={text} aria-label={text}
plain data-current={current || undefined}
className={clsx(styles.link, current && styles.current, className)} 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} {...rest}
> >
{item} {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 Link from "../Link";
import type { ComponentPropsWithoutRef } from "react"; import type { ComponentPropsWithoutRef } from "react";
import styles from "./PageTitle.module.css";
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & { export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
canonical: string; canonical: string;
}; };
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => { const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
return ( return (
<h1 className={clsx(styles.title, className)} {...rest}> <h1 className={cn("mt-1 mb-6 text-left text-3xl font-medium lowercase", className)} {...rest}>
<Link href={canonical} plain className={styles.slug}> <Link
href={canonical}
className="before:text-medium-light before:mr-[-0.1em] before:tracking-widest before:content-['\002E\002F']"
>
{children} {children}
</Link> </Link>
</h1> </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 { ComponentPropsWithoutRef } from "react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import styles from "./ThemeToggle.module.css";
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>; export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => { const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
@ -17,10 +15,10 @@ const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
<button <button
onClick={() => setTheme(theme === "light" ? "dark" : "light")} onClick={() => setTheme(theme === "light" ? "dark" : "light")}
aria-label="Toggle Theme" 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} /> <SunIcon className={clsx("!block dark:!hidden", className)} {...rest} />
<MoonIcon className={clsx(styles.moon, className)} {...rest} /> <MoonIcon className={clsx("!hidden dark:!block", className)} {...rest} />
</button> </button>
); );
}; };

View File

@ -2,4 +2,4 @@
export const POSTS_DIR = "notes"; export const POSTS_DIR = "notes";
/** Maximum width of content wrapper (e.g. for images) in pixels. */ /** 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": { "dependencies": {
"@date-fns/tz": "^1.2.0", "@date-fns/tz": "^1.2.0",
"@date-fns/utc": "^2.1.0", "@date-fns/utc": "^2.1.0",
"@emotion/hash": "^0.9.2",
"@giscus/react": "^3.1.0", "@giscus/react": "^3.1.0",
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
@ -41,7 +40,6 @@
"html-entities": "^2.6.0", "html-entities": "^2.6.0",
"lucide-react": "0.503.0", "lucide-react": "0.503.0",
"next": "15.4.0-canary.5", "next": "15.4.0-canary.5",
"polished": "^4.3.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "19.1.0", "react": "19.1.0",
"react-activity-calendar": "^2.7.10", "react-activity-calendar": "^2.7.10",
@ -69,6 +67,8 @@
"resend": "^4.4.0", "resend": "^4.4.0",
"server-only": "0.0.1", "server-only": "0.0.1",
"shiki": "^3.3.0", "shiki": "^3.3.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
"unified": "^11.0.5", "unified": "^11.0.5",
"valibot": "^1.0.0" "valibot": "^1.0.0"
}, },
@ -76,6 +76,7 @@
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.25.1", "@eslint/js": "^9.25.1",
"@jakejarvis/eslint-config": "^4.0.7", "@jakejarvis/eslint-config": "^4.0.7",
"@tailwindcss/postcss": "^4.1.4",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"@types/prop-types": "^15.7.14", "@types/prop-types": "^15.7.14",
@ -97,12 +98,10 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^15.5.1", "lint-staged": "^15.5.1",
"postcss": "^8.5.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"schema-dts": "^1.1.5", "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" "typescript": "5.8.3"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -119,9 +118,6 @@
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx,md,mdx}": [ "*.{js,jsx,ts,tsx,md,mdx}": [
"eslint" "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", "groupName": "react",
"rangeStrategy": "pin" "rangeStrategy": "pin"
}, },
{
"matchPackageNames": ["tailwindcss", "@tailwindcss/*", "tailwind-merge", "prettier-plugin-tailwindcss"],
"groupName": "tailwindcss"
},
{ {
"matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"], "matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"],
"groupName": "mdx" "groupName": "mdx"
@ -32,8 +36,8 @@
"groupName": "eslint" "groupName": "eslint"
}, },
{ {
"matchPackageNames": ["stylelint", "stylelint-*"], "matchPackageNames": ["prettier", "prettier-*", "!prettier-plugin-tailwindcss"],
"groupName": "stylelint" "groupName": "prettier"
}, },
{ {
"matchPackageNames": ["typescript"], "matchPackageNames": ["typescript"],