mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 08:05:23 -04:00
initial tailwind conversion
This commit is contained in:
parent
4cc25f7ab9
commit
72446b0c47
@ -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
1
.npmrc
@ -1,3 +1,2 @@
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
public-hoist-pattern[]=*stylelint*
|
||||
|
@ -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;
|
||||
|
@ -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
8
.vscode/extensions.json
vendored
Normal 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
16
.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
@ -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 {
|
||||
|
@ -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>.
|
||||
|
@ -32,7 +32,7 @@ const Page = () => {
|
||||
size="0.975em"
|
||||
style={{
|
||||
marginRight: "0.15em",
|
||||
stroke: "var(--colors-warning)",
|
||||
stroke: "var(--color-warning)",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>{" "}
|
||||
|
@ -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,
|
||||
});
|
||||
|
153
app/globals.css
153
app/globals.css
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--fonts-sans);
|
||||
background-color: var(--colors-background-inner);
|
||||
}
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-size: 1em;
|
||||
font-family: var(--fonts-mono);
|
||||
font-variant-ligatures: none; /* i hate them. fwiw. */
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-appearance: button;
|
||||
&: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)
|
||||
}
|
||||
|
@ -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{" "}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}>
|
||||
<SkipNavTarget />
|
||||
{children}
|
||||
</div>
|
||||
<main className="bg-background-inner w-full">
|
||||
<SkipNavTarget />
|
||||
<div className="max-w-default mx-auto p-5">{children}</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
@ -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{" "}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
167
app/page.tsx
167
app/page.tsx
@ -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’m Jake. <span className={styles.wave}>👋</span>
|
||||
<>
|
||||
<h1 className="mb-2 text-3xl leading-7 font-medium">
|
||||
Hi there! I’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’m a frontend web developer based in the{" "}
|
||||
<Link
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&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’ve improved a bit since then, I think? 🤷</span>
|
||||
. <span className="text-gray-500">I’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'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’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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -85,7 +85,7 @@ _Previously on the [Cringey Chronicles™](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))_
|
||||
|
@ -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;
|
||||
}
|
@ -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,
|
||||
}}
|
||||
// invisible icon hack to fix line height
|
||||
className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
|
||||
/>
|
||||
<span>
|
||||
Updated <RelativeTime date={repo!.pushedAt} />
|
||||
@ -120,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>{" "}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
<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 />
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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" />
|
||||
)}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
8
lib/helpers/classnames.ts
Normal file
8
lib/helpers/classnames.ts
Normal 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;
|
@ -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.
|
||||
|
14
package.json
14
package.json
@ -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
1108
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -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"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user