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

initial tailwind conversion

This commit is contained in:
Jake Jarvis 2025-04-26 01:11:49 -04:00
parent 4cc25f7ab9
commit 72446b0c47
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
56 changed files with 834 additions and 1751 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

@ -2,15 +2,15 @@
width: 100%;
padding: 0.8em;
margin: 0.6em 0;
border: 2px solid var(--colors-light);
border: 2px solid var(--color-gray-400);
border-radius: 0.6em;
color: var(--colors-text);
background-color: var(--colors-super-duper-light);
background-color: var(--color-gray-100);
}
.input:focus {
outline: none;
border-color: var(--colors-link);
border-color: var(--color-link);
}
.input.textarea {
@ -21,12 +21,12 @@
}
.input.invalid {
border-color: var(--colors-error);
border-color: var(--color-error);
}
.errorMessage {
font-size: 0.9em;
color: var(--colors-error);
color: var(--color-error);
}
.actionRow {
@ -48,13 +48,13 @@
user-select: none;
font-weight: 500;
color: var(--colors-text);
background-color: var(--colors-kinda-light);
background-color: var(--color-gray-300);
}
.submitButton:hover,
.submitButton:focus-visible {
color: var(--colors-super-duper-light);
background-color: var(--colors-link);
color: var(--color-gray-100);
background-color: var(--color-link);
}
.submitIcon {
@ -70,11 +70,11 @@
}
.result.success {
color: var(--colors-success);
color: var(--color-success);
}
.result.error {
color: var(--colors-error);
color: var(--color-error);
}
.resultIcon {

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

@ -32,7 +32,7 @@ const Page = () => {
size="0.975em"
style={{
marginRight: "0.15em",
stroke: "var(--colors-warning)",
stroke: "var(--color-warning)",
verticalAlign: "middle",
}}
/>{" "}

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,98 @@
/*!
* 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);
--container-default: var(--container-4xl);
}
html {
line-height: 1.15;
tab-size: 4;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-text-size-adjust: 100%;
@theme {
--color-*: initial;
--color-background-inner: oklch(1 0 0);
--color-background-outer: oklch(0.99 0 0);
--color-gray-900: oklch(0.23 0 0);
--color-gray-800: oklch(0.32 0 0);
--color-gray-700: oklch(0.43 0 0);
--color-gray-600: oklch(0.48 0 0);
--color-gray-500: oklch(0.56 0 0);
--color-gray-400: oklch(0.66 0 0);
--color-gray-300: oklch(0.78 0 0);
--color-gray-200: oklch(0.85 0 0);
--color-gray-100: oklch(0.99 0 0);
--color-link: oklch(0.53 0.1547 252.33);
--color-success: oklch(0.63 0.1557 144.2);
--color-warning: oklch(0.72 0.177693 55.7508);
--color-error: oklch(0.64 0.2505 28.39);
--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);
}
100% {
transform: rotate(0deg);
}
}
body {
margin: 0;
font-family: var(--fonts-sans);
background-color: var(--colors-background-inner);
@keyframes heartbeat {
0% {
transform: scale(1);
}
2% {
transform: scale(1.25);
}
4% {
transform: scale(1);
}
6% {
transform: scale(1.2);
}
8% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
}
code,
kbd,
samp,
pre {
font-size: 1em;
font-family: var(--fonts-mono);
font-variant-ligatures: none; /* i hate them. fwiw. */
}
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
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;
&:where([data-theme=dark], [data-theme=dark] *) {
--color-background-inner: oklch(0.24 0 0);
--color-background-outer: oklch(0.26 0 0);
--color-gray-900: oklch(0.93 0 0);
--color-gray-800: oklch(0.90 0 0);
--color-gray-700: oklch(0.88 0 0);
--color-gray-600: oklch(0.76 0 0);
--color-gray-500: oklch(0.67 0 0);
--color-gray-400: oklch(0.5 0 0);
--color-gray-300: oklch(0.44 0 0);
--color-gray-200: oklch(0.35 0 0);
--color-gray-100: oklch(0.24 0 0);
--color-link: oklch(0.81 0.1026 246.31);
--color-success: oklch(0.81 0.2001 138.65);
--color-warning: oklch(0.81 0.166 85.03);
--color-error: oklch(0.68 0.2105 24.73)
}

View File

@ -57,7 +57,7 @@ const Page = () => {
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-medium-light)",
color: "var(--color-gray-500)",
}}
>
Video is property of{" "}

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 font-sans text-gray-900">
<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}>
<main className="bg-background-inner w-full">
<SkipNavTarget />
{children}
</div>
<div className="max-w-default mx-auto p-5">{children}</div>
</main>
<Footer />

View File

@ -56,7 +56,7 @@ const Page = () => {
fontSize: "0.9em",
lineHeight: 1.8,
margin: "1.25em 1em 0 1em",
color: "var(--colors-medium-light)",
color: "var(--color-gray-500)",
}}
>
Video is property of{" "}

View File

@ -17,15 +17,15 @@ const Page = () => {
<Video
src="https://ijyxfbpcm3itvdly.public.blob.vercel-storage.com/not-found-SAtLyNyc7gVhveYxr6o1ITd9CSXo5X.mp4"
autoPlay
style={{ maxWidth: 480, aspectRatio: "16/11" }}
className="aspect-[16/11] max-w-[480px]"
/>
<div style={{ textAlign: "center", marginTop: "1.5em" }}>
<h1 style={{ margin: "0.5em 0", fontSize: "2.2em", fontWeight: 500, lineHeight: 1 }}>Page Not Found</h1>
<div className="mt-6 text-center">
<h1 className="my-2 text-3xl font-medium">Page Not Found</h1>
<Link href="/" style={{ fontSize: "1.2em", fontWeight: 500 }}>
Go home?
</Link>
<p className="mt-4 mb-0 text-xl font-medium">
<Link href="/">Go home?</Link>
</p>
</div>
</>
);

View File

@ -1,77 +0,0 @@
.meta {
display: flex;
justify-items: flex-start;
flex-wrap: wrap;
font-size: 0.925em;
line-height: 2.3;
letter-spacing: 0.04em;
color: var(--colors-medium);
}
.meta .metaItem {
display: inline-flex;
align-items: center;
margin-right: 1.6em;
white-space: nowrap;
}
.meta .metaLink {
color: inherit;
}
.meta .metaIcon {
width: 1.25em;
height: 1.25em;
margin-right: 0.6em;
flex-shrink: 0;
}
.meta .metaTags {
display: inline-flex;
flex-wrap: wrap;
white-space: normal;
}
.meta .metaTag {
text-transform: lowercase;
white-space: nowrap;
margin-right: 0.75em;
}
.meta .metaTag::before {
content: "\0023"; /* cosmetically hashtagify tags */
padding-right: 0.125em;
color: var(--colors-light);
}
.meta .metaTag:last-of-type {
margin-right: 0;
}
.title {
margin: 0.3em 0 0.5em -1px; /* misaligned left margin, super nitpicky */
font-size: 2.3em;
line-height: 1.3;
font-weight: 700;
}
.title code {
margin: 0 0.075em;
}
.title .link {
color: var(--colors-text);
}
.comments {
margin-top: 2em;
padding-top: 2em;
border-top: 2px solid var(--colors-light);
min-height: 140px;
}
@media (max-width: 768px) {
.title {
font-size: 1.9em;
}
}

View File

@ -1,7 +1,6 @@
import { env } from "../../../lib/env";
import { Suspense } from "react";
import { JsonLd } from "react-schemaorg";
import clsx from "clsx";
import { CalendarDaysIcon, TagIcon, SquarePenIcon, EyeIcon } from "lucide-react";
import Link from "../../../components/Link";
import Time from "../../../components/Time";
@ -16,8 +15,6 @@ import { size as ogImageSize } from "./opengraph-image";
import type { Metadata } from "next";
import type { BlogPosting } from "schema-dts";
import styles from "./page.module.css";
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params#disable-rendering-for-unspecified-paths
export const dynamicParams = false;
@ -87,18 +84,26 @@ 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)}>
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
<div className="flex flex-wrap justify-items-start text-[0.85rem] leading-9 tracking-wider text-gray-600">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
className={"mr-6 inline-flex items-center whitespace-nowrap text-inherit hover:no-underline"}
>
<CalendarDaysIcon size="1.25em" className="mr-2.5 h-[1.25em] w-[1.25em] shrink-0" />
<Time date={frontmatter!.date} format="MMMM d, y" />
</Link>
{frontmatter!.tags && (
<div className={styles.metaItem}>
<TagIcon size="1.25em" className={styles.metaIcon} />
<span className={styles.metaTags}>
<div className="mr-6 inline-flex items-center whitespace-nowrap">
<TagIcon size="1.25em" className="mr-2.5 h-[1.25em] w-[1.25em] shrink-0" />
<span className="inline-flex flex-wrap whitespace-normal">
{frontmatter!.tags.map((tag) => (
<span key={tag} title={tag} className={styles.metaTag} aria-label={`Tagged with ${tag}`}>
<span
key={tag}
title={tag}
className="mr-3 whitespace-nowrap lowercase before:pr-0.5 before:text-gray-400 before:content-['\0023'] last-of-type:mr-0"
aria-label={`Tagged with ${tag}`}
>
{tag}
</span>
))}
@ -109,22 +114,14 @@ 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={"hover:no-underlinem mr-6 inline-flex items-center whitespace-nowrap text-inherit"}
>
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
<SquarePenIcon size="1.25em" className="mr-2.5 h-[1.25em] w-[1.25em] shrink-0" />
<span>Improve This Post</span>
</Link>
<div
className={styles.metaItem}
style={{
// fix potential layout shift when number of hits loads
minWidth: "6em",
marginRight: 0,
}}
>
<EyeIcon size="1.25em" className={styles.metaIcon} />
<div className="mr-0 inline-flex min-w-2.5 items-center whitespace-nowrap">
<EyeIcon size="1.25em" className="mr-2.5 h-[1.25em] w-[1.25em] shrink-0" />
<Suspense
// when this loads, the component will count up from zero to the actual number of hits, so we can simply
// show a zero here as a "loading indicator"
@ -135,20 +132,19 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
</div>
</div>
<h1 className={styles.title}>
<h1 className="mt-2 mb-3 text-3xl/10 font-bold md:text-4xl/12 [&_code]:mx-0.5">
<Link
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
plain
className={styles.link}
className="text-gray-800 hover:no-underline"
/>
</h1>
<MDXContent />
{!frontmatter!.noComments && (
<div id="comments" className={styles.comments}>
<Suspense fallback={<Loading boxes={3} width={40} style={{ display: "block", margin: "2em auto" }} />}>
<div id="comments" className="mt-8 min-h-36 border-t-2 border-solid border-t-gray-400 pt-8">
<Suspense fallback={<Loading boxes={3} width={40} className="mx-auto my-8 block" />}>
<Comments title={frontmatter!.title} />
</Suspense>
</div>

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,54 @@
import clsx from "clsx";
import hash from "@emotion/hash";
import { rgba } from "polished";
import Link from "../components/Link";
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-7 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-7 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="[--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"
>
<Link href="https://www.typescriptlang.org/" className="[--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="[--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="[--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="[--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="[--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="[--color-link:#8892bf] dark:[--color-link:#a4afe3]"
>
less buzzwordy
</Link>{" "}
@ -107,21 +56,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="[--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="[--color-link:#00b81a] dark:[--color-link:#57f06d]"
>
application security
</Link>
@ -129,8 +76,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="[--color-link:#0098ec] dark:[--color-link:#43b9fb]"
>
serverless stacks
</Link>
@ -138,21 +84,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="[--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="[--color-link:#4169e1] dark:[--color-link:#8ca9ff]"
>
frontend web design
</Link>{" "}
@ -160,8 +104,7 @@ const Page = () => {
<Link
href="/notes/my-first-code"
title="Jake's Bulletin Board, circa 2003"
lightColor="#9932cc"
darkColor="#d588fb"
className="[--color-link:#9932cc] dark:[--color-link:#d588fb]"
>
backend programming
</Link>{" "}
@ -169,86 +112,76 @@ const Page = () => {
<Link
href="/birthday"
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
lightColor="#e40088"
darkColor="#fd40b1"
className="[--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-gray-500">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="[--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="[--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="[--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="[--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="[--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="[--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="[--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="[--color-link:#8d4eff] dark:[--color-link:#a379f0]"
>
GitHub
</Link>{" "}
@ -256,44 +189,30 @@ const Page = () => {
<Link
href="https://www.linkedin.com/in/jakejarvis/"
rel="me"
title="Jake Jarvis on LinkedIn"
lightColor="#0073b1"
darkColor="#3b9dd2"
className="[--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="[--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="[--color-link:#757575] hover:no-underline dark:[--color-link:#959595]"
>
<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="[--color-link:#0085ff] dark:[--color-link:#208bfe]"
>
Bluesky
</Link>
@ -301,15 +220,13 @@ const Page = () => {
<Link
href="https://fediverse.jarv.is/@jake"
rel="me"
title="Jake Jarvis on Mastodon"
lightColor="#6d6eff"
darkColor="#7b87ff"
className="[--color-link:#6d6eff] dark:[--color-link:#7b87ff]"
>
Mastodon
</Link>{" "}
as well!
</p>
</div>
</>
);
};

View File

@ -85,7 +85,7 @@ _Previously on the [Cringey Chronicles&trade;](https://web.archive.org/web/20010
<iframe
src="https://jakejarvis.github.io/my-first-website/"
title="My Terrible, Horrible, No Good, Very Bad First Website"
style={{ height: "500px", width: "100%", border: "1px solid var(--colors-kinda-light)", marginBottom: "-0.4em" }}
style={{ height: "500px", width: "100%", border: "1px solid var(--color-gray-300)", marginBottom: "-0.4em" }}
/>
_[November 2001](https://jakejarvis.github.io/my-first-website/) ([view
source](https://github.com/jakejarvis/my-first-website))_

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,52 @@ 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-xl 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-gray-600 [&_.react-activity-calendar\_\_legend-colors]:text-gray-500 [&_.react-activity-calendar\_\_legend-month]:text-gray-600`
)}
>
<Calendar data={contributions} />
</div>
</Suspense>
<h2 className={styles.heading}>
<h2 className="my-3.5 text-xl 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="row-auto grid w-full grid-cols-none gap-4 lg:grid-cols-2">
{repos?.map((repo) => (
<div key={repo!.name} className={styles.card}>
<Link href={repo!.url} className={styles.name}>
<div key={repo!.name} className="h-fit rounded-2xl border border-solid border-gray-300 p-4 text-gray-700">
<Link href={repo!.url} className="mb-1.5 inline-block text-base font-semibold">
{repo!.name}
</Link>
{repo!.description && <p className={styles.description}>{repo!.description}</p>}
{repo!.description && <p className="m-0 text-sm leading-relaxed">{repo!.description}</p>}
<div className={styles.meta}>
<div className="mt-2 flex flex-wrap text-sm">
{repo!.primaryLanguage && (
<div className={styles.metaItem}>
<div className="mt-1.5 mr-5 whitespace-nowrap text-gray-600">
{repo!.primaryLanguage.color && (
<span
className={styles.metaIcon}
className="mr-2 inline-block h-[1.25em] w-[1.25em] align-text-top"
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
/>
)}
@ -75,41 +85,35 @@ const Page = async () => {
)}
{repo!.stargazerCount > 0 && (
<div className={styles.metaItem}>
<div className="mt-1.5 mr-5 whitespace-nowrap text-gray-600">
<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-2 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="mt-1.5 mr-5 whitespace-nowrap text-gray-600">
<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-2 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="mt-1.5 whitespace-nowrap text-gray-600">
<span
className={styles.metaIcon}
style={{
// invisible icon hack to fix line height
width: 0,
marginRight: 0,
}}
className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
/>
<span>
Updated <RelativeTime date={repo!.pushedAt} />
@ -120,27 +124,15 @@ const Page = async () => {
))}
</div>
<p
style={{
textAlign: "center",
marginBottom: 0,
fontWeight: 500,
}}
>
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
<p className="mt-4 mb-0 text-center text-base font-medium">
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} className="hover:no-underline">
View more on{" "}
<svg
xmlns="http://www.w3.org/2000/svg"
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="ml-1 inline h-[1.2em] w-[1.2em] fill-gray-700 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

@ -1,37 +0,0 @@
:root {
--colors-background-inner: #ffffff;
--colors-background-outer: #fcfcfc;
--colors-background-header: rgb(252 252 252 / 70%);
--colors-text: #202020;
--colors-medium-dark: #515151;
--colors-medium: #5e5e5e;
--colors-medium-light: #757575;
--colors-light: #d2d2d2;
--colors-kinda-light: #e3e3e3;
--colors-super-light: #f4f4f4;
--colors-super-duper-light: #fbfbfb;
--colors-link: #0e6dc2;
--colors-link-underline: #a6c5e7;
--colors-success: #44a248;
--colors-error: #ff1b1b;
--colors-warning: #f78200;
}
[data-theme="dark"] {
--colors-background-inner: #1e1e1e;
--colors-background-outer: #252525;
--colors-background-header: rgb(37 37 37 / 70%);
--colors-text: #f1f1f1;
--colors-medium-dark: #d7d7d7;
--colors-medium: #b1b1b1;
--colors-medium-light: #959595;
--colors-light: #646464;
--colors-kinda-light: #535353;
--colors-super-light: #272727;
--colors-super-duper-light: #1f1f1f;
--colors-link: #88c7ff;
--colors-link-underline: #496278;
--colors-success: #78df55;
--colors-error: #ff5151;
--colors-warning: #f2b702;
}

View File

@ -32,20 +32,20 @@ const Page = () => {
>
<code
style={{
backgroundColor: "var(--colors-background-header)",
backgroundColor: "var(--color-background-header)",
backdropFilter: "saturate(180%) blur(5px))",
display: "block",
overflowX: "auto",
padding: "1em",
fontSize: "0.9em",
tabSize: 2,
border: "1px solid var(--colors-kinda-light)",
border: "1px solid var(--color-gray-300)",
borderRadius: "0.6em",
}}
>
<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 ml-0 border-l-4 pl-5 text-gray-700", className)} {...rest} />
);
export default Blockquote;

View File

@ -7,8 +7,8 @@
font-size: 0.925em;
tab-size: 2px;
page-break-inside: avoid;
background-color: var(--colors-background-header);
border: 1px solid var(--colors-kinda-light);
background-color: var(--color-background-header);
border: 1px solid var(--color-gray-300);
border-radius: 0.6em;
}
@ -55,7 +55,7 @@ figure .code[data-line-numbers] > [data-line]::before {
width: 1em;
margin-right: 1.5em;
text-align: right;
color: var(--colors-medium-light);
color: var(--color-gray-500);
user-select: none;
counter-increment: line;
content: counter(line);
@ -76,11 +76,11 @@ figure .code[data-line-numbers-max-digits="3"] > [data-line]::before {
height: 3em;
width: 3em;
padding: 0; /* iOS safari fix */
color: var(--colors-medium-dark);
border: 1px solid var(--colors-kinda-light);
color: var(--color-gray-700);
border: 1px solid var(--color-gray-300);
border-top-right-radius: 0.6em;
border-bottom-left-radius: 0.6em;
background-color: var(--colors-background-header);
background-color: var(--color-background-header);
backdrop-filter: saturate(180%) blur(5px);
}
@ -92,5 +92,5 @@ figure .code[data-line-numbers-max-digits="3"] > [data-line]::before {
.copyButton:hover,
.copyButton:focus-visible {
color: var(--colors-link);
color: var(--color-link);
}

View File

@ -52,7 +52,7 @@ const CopyButton = ({ source, timeout = 2000, style, ...rest }: CopyButtonProps,
{...rest}
>
{copied ? (
<CheckIcon size="1.25em" style={{ stroke: "var(--colors-success)" }} />
<CheckIcon size="1.25em" style={{ stroke: "var(--color-success)" }} />
) : (
<ClipboardIcon size="1.25em" />
)}

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("bg-background-outer w-full border-t border-t-gray-300 py-4 text-gray-700", 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-gray-700 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-gray-700 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-gray-700 hover:text-gray-600 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-2 border-b-gray-400 pb-0.5 text-gray-700 hover:border-b-gray-300 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 sticky top-0 z-100 h-24 w-full border-b border-gray-300 backdrop-blur-xs 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="hover:text-link flex flex-shrink-0 items-center text-gray-700 hover:no-underline"
>
<Image
src={avatarImg}
alt={`Photo of ${config.authorName}`}
className={styles.avatar}
className="h-[70px] w-[70px] rounded-full border-2 border-gray-400 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

@ -12,7 +12,7 @@
.h.divider {
padding-bottom: 0.25em;
border-bottom: 1px solid var(--colors-kinda-light);
border-bottom: 1px solid var(--color-gray-300);
}
.anchor {
@ -27,7 +27,7 @@
/* show anchor link when hovering anywhere over the heading line, or on keyboard tab focus */
.anchor:hover,
.anchor:focus-visible {
color: var(--colors-link) !important;
color: var(--color-link) !important;
}
.h:hover .anchor,

View File

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

View File

@ -3,5 +3,5 @@
max-width: calc(var(--max-width) - 1.5em);
height: 1px;
border: 0;
background-color: var(--colors-light);
background-color: var(--color-gray-400);
}

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

@ -7,7 +7,7 @@
display: inline-block;
height: 100%;
animation: loading 1.5s infinite ease-in-out both;
background-color: var(--colors-medium-light);
background-color: var(--color-gray-500);
}
@keyframes loading {

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,34 @@
"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 max-w-1/2 flex-row justify-between sm:max-w-2/3 md:max-w-none 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 Link from "../Link";
import cn from "../../lib/helpers/classnames";
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(
"-mb-[0.2em] inline-flex items-center p-2.5 text-gray-700 hover:border-b-[0.2em] hover:border-gray-300 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 Link from "../Link";
import cn from "../../lib/helpers/classnames";
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:mr-[-0.1em] before:tracking-widest before:text-gray-500 before:content-['\002E\002F'] hover:no-underline"
>
{children}
</Link>
</h1>

View File

@ -1,29 +0,0 @@
/*!
* @reach/skip-nav | MIT License | https://github.com/reach/reach-ui/blob/v0.18.0/packages/skip-nav/styles.css
*/
.hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
width: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
position: absolute;
}
.hidden:focus {
padding: 1rem;
position: fixed;
top: 10px;
left: 10px;
z-index: 99999;
width: auto;
height: auto;
clip: auto;
background: var(--colors-super-duper-light);
color: var(--colors-link);
border: 2px solid var(--colors-kinda-light);
text-decoration: underline;
}

View File

@ -1,10 +1,12 @@
import styles from "./SkipNav.module.css";
const skipNavId = "skip-nav";
export const SkipNavLink = () => {
return (
<a href={`#${skipNavId}`} tabIndex={0} className={styles.hidden}>
<a
href={`#${skipNavId}`}
tabIndex={0}
className="text-link absolute z-[99999] -m-px h-0 w-0 overflow-hidden border-0 border-solid border-gray-300 p-0 underline [clip:rect(0_0_0_0)] focus:fixed focus:top-2.5 focus:left-2.5 focus:h-auto focus:w-auto focus:border-2 focus:p-4 focus:[clip:auto]"
>
Skip to content
</a>
);

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

@ -34,7 +34,7 @@ I've written a simple implementation below, which...
<iframe
src="https://jakejarvis.github.io/dark-mode-example/"
title="Dark Mode Example"
style={{ height: "190px", width: "100%", border: "1px solid var(--colors-kinda-light)" }}
style={{ height: "190px", width: "100%", border: "1px solid var(--color-gray-300)" }}
></iframe>
A _very_ barebones example is embedded above ([view the source here](https://github.com/jakejarvis/dark-mode-example), or [open in a new window](https://jakejarvis.github.io/dark-mode-example/) if your browser is blocking the frame) and you can try it out on this site by clicking the 💡 lightbulb in the upper right corner of this page. You'll notice that the dark theme sticks when refreshing this page, navigating between other pages, or if you were to return to this example weeks from now.

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.10",
"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.1",
"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.1",
"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"],