Merge pull request #1 from jaronpate/ft/2000s-style

2000s style
This commit is contained in:
2026-08-22 13:21:50 -05:00
committed by GitHub
85 changed files with 1547 additions and 1212 deletions
-3
View File
@@ -1,5 +1,4 @@
<script setup lang="ts">
import '~/assets/css/main.css';
import {
DEFAULT_DESCRIPTION,
DEFAULT_IMAGE,
@@ -41,8 +40,6 @@ useHead({
},
],
});
onMounted(() => {});
</script>
<template>
+132 -71
View File
@@ -1,13 +1,22 @@
/* Sohne Buch */
/*@font-face {
font-family: 'SohneBuch';
src:
url('~/assets/fonts/Sohne/Sohne-Buch.woff') format('woff'),
url('~/assets/fonts/Sohne/Sohne-Buch.ttf') format('truetype');
font-display: swap;
}*/
/* Plantin MT Pro */
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProLight.TTF')
format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProLightIt.TTF')
format('truetype');
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProRg.TTF')
@@ -46,23 +55,44 @@
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProLight.TTF')
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProSmBd.TTF')
format('truetype');
font-weight: 300;
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProLightIt.TTF')
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProSemiBdIt.TTF')
format('truetype');
font-weight: 300;
font-weight: 600;
font-style: italic;
font-display: swap;
}
/* Redaction */
@font-face {
font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProCnBd.TTF')
format('truetype');
font-weight: 700;
font-style: normal;
font-stretch: condensed;
font-display: swap;
}
/* Sohne Buch */
@font-face {
font-family: 'SohneBuch';
src:
url('~/assets/fonts/Sohne/Sohne-Buch.woff') format('woff'),
url('~/assets/fonts/Sohne/Sohne-Buch.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Redaction — 7 grades × Regular / Italic / Bold */
@font-face {
font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Regular.woff2') format('woff2');
@@ -70,13 +100,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Italic.woff2') format('woff2');
@@ -85,7 +109,14 @@
font-display: swap;
}
/* Redaction 10 */
@font-face {
font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.woff2')
@@ -94,13 +125,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Italic.woff2')
@@ -110,7 +135,14 @@
font-display: swap;
}
/* Redaction 20 */
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Regular.woff2')
@@ -119,13 +151,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Italic.woff2')
@@ -135,7 +161,14 @@
font-display: swap;
}
/* Redaction 35 */
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Regular.woff2')
@@ -144,13 +177,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Italic.woff2')
@@ -160,7 +187,14 @@
font-display: swap;
}
/* Redaction 50 */
@font-face {
font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.woff2')
@@ -169,13 +203,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2')
@@ -185,7 +213,14 @@
font-display: swap;
}
/* Redaction 70 */
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2')
@@ -194,13 +229,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Italic.woff2')
@@ -210,7 +239,14 @@
font-display: swap;
}
/* Redaction 100 */
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Regular.woff2')
@@ -219,14 +255,7 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Bold.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Italic.woff2')
@@ -236,4 +265,36 @@
font-display: swap;
}
/*@import url('https://fonts.googleapis.com/css2?family=Young+Serif&display=swap');*/
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Space Mono */
@font-face {
font-family: 'Space Mono';
src: url('~/assets/fonts/Space Mono/SpaceMono-Regular.ttf')
format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Space Mono';
src: url('~/assets/fonts/Space Mono/SpaceMono-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Space Mono';
src: url('~/assets/fonts/Space Mono/SpaceMono-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
+341 -94
View File
@@ -1,163 +1,410 @@
@import './fonts.css';
:root {
/*--font: 'Baskervville', serif;*/
/*--font: 'EB Garamond', serif;*/
--font: 'Plantin MT Pro', serif;
/*--font: 'Young Serif', serif;*/
/*--font: 'SohneBuch', sans-serif;*/
/*--font-plain: 'SohneBuch', sans-serif;*/
/*--font: 'Redaction', serif;*/
/*--font-hover: 'Redaction 70', serif;*/
--font-plain: var(--font);
--font-hover: var(--font);
--text: #333;
--muted: #666;
--font: 'Plantin MT Pro', Georgia, serif;
--font-plain: 'SohneBuch', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'Space Mono', ui-monospace, 'SF Mono', Menlo, monospace;
/* paper + ink from the old site; accent/navy sampled from blend.jpeg */
--bg: #f6f7f9;
--bg-secondary: #e9ebef;
--dot-color: #666;
--color-white-500: #f6f7f9;
--text: #2e3140;
--muted: #6a6d7a;
--accent: #c45d52;
--accent-soft: #e78c88;
--navy: #2a2c61;
--hairline: #d8dbe2;
--code-bg: #f3e4e2;
--img-matte: #e9ebef;
--body-weight: 300;
--measure: 38rem;
--note-width: 13rem;
--note-gap: 2rem;
--page-pad: 1.25rem;
--col: min(var(--measure), calc(100vw - var(--page-pad) * 2));
--cluster: calc(var(--col) + var(--note-gap) + var(--note-width));
--col-left: max(var(--page-pad), calc((100vw - var(--cluster)) / 2));
--gap: 1rem;
--radius: 4px;
}
::selection {
background: var(--text);
color: var(--bg);
html.font-plantin {
--font: 'Plantin MT Pro', Georgia, serif;
--body-weight: 300;
}
.dark-mode {
html.font-sohne {
--font: 'SohneBuch', -apple-system, BlinkMacSystemFont, sans-serif;
--body-weight: 400;
}
html.font-space {
--font: 'Space Mono', ui-monospace, Menlo, monospace;
--body-weight: 400;
}
html.font-georgia {
--font: Georgia, 'Times New Roman', Times, serif;
--body-weight: 400;
}
html.font-palatino {
--font: Palatino, 'Palatino Linotype', 'Book Antiqua', serif;
--body-weight: 400;
}
html.font-redaction {
--font: 'Redaction', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-10 {
--font: 'Redaction 10', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-20 {
--font: 'Redaction 20', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-35 {
--font: 'Redaction 35', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-50 {
--font: 'Redaction 50', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-70 {
--font: 'Redaction 70', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-100 {
--font: 'Redaction 100', Georgia, serif;
--body-weight: 400;
}
html.font-system {
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--body-weight: 400;
}
.dark-mode,
html.theme-navy,
html.theme-dusk,
html.theme-midnight,
html.theme-indigo,
html.theme-storm,
html.theme-slate,
html.theme-ink,
html.theme-pine,
html.theme-wine,
html.theme-cocoa {
--text: #fbfbf7;
--muted: #9aa4b8;
--accent: #e78c88;
--accent-soft: #c45d52;
--navy: #3c48a8;
}
html.theme-paper {
--bg: #f6f7f9;
--bg-secondary: #e9ebef;
--text: #2e3140;
--muted: #6a6d7a;
--accent: #c45d52;
--accent-soft: #e78c88;
--navy: #2a2c61;
--hairline: #d8dbe2;
--code-bg: #f3e4e2;
--img-matte: #e9ebef;
}
html.theme-navy {
--bg: #12192c;
--bg-secondary: #1a2238;
--hairline: #2a334c;
--code-bg: #221e34;
--img-matte: #1a2238;
}
html.theme-dusk {
--bg: #1a2236;
--bg-secondary: #252d44;
--hairline: #343e58;
--code-bg: #2c2840;
--img-matte: #252d44;
}
html.theme-midnight {
--bg: #0e1424;
--bg-secondary: #161d30;
--hairline: #242c42;
--code-bg: #1c1a2c;
--img-matte: #161d30;
}
html.theme-indigo {
--bg: #16142a;
--bg-secondary: #1f1c38;
--hairline: #2e2a4a;
--code-bg: #241e38;
--img-matte: #1f1c38;
}
html.theme-storm {
--bg: #1c2438;
--bg-secondary: #283044;
--hairline: #3a4458;
--code-bg: #2c2840;
--img-matte: #283044;
}
html.theme-slate {
--bg: #1e212b;
--bg-secondary: #2a2d3a;
--hairline: #3a3e4c;
--code-bg: #322833;
--img-matte: #2a2d3a;
}
html.theme-ink {
--bg: #0c0c0c;
--bg-secondary: #1c1c1c;
--dot-color: #666;
--muted: #a3a3a3;
--color-white-500: #fbfbf7;
--hairline: #2a2a2a;
--code-bg: #221c20;
--img-matte: #1c1c1c;
}
.flex {
display: flex;
html.theme-pine {
--bg: #121c22;
--bg-secondary: #1a262c;
--hairline: #2a3840;
--code-bg: #1c2428;
--img-matte: #1a262c;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
html.theme-wine {
--bg: #1a1520;
--bg-secondary: #261c28;
--hairline: #3a2c38;
--code-bg: #2a1e28;
--img-matte: #261c28;
}
.flex-ff {
flex: 1;
}
.row {
flex-direction: row;
}
.col {
flex-direction: column;
html.theme-cocoa {
--bg: #1c1814;
--bg-secondary: #2a241c;
--hairline: #3c3428;
--code-bg: #2c241c;
--img-matte: #2a241c;
}
* {
box-sizing: border-box;
}
::selection {
background: var(--accent);
color: #fff;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
overflow-x: clip;
overscroll-behavior-y: none;
overscroll-behavior-x: auto;
}
body {
font-family: var(--font);
color: var(--text);
font-size: 16px;
line-height: 1.6;
line-height: 1.65;
font-weight: var(--body-weight);
}
strong,
b {
font-weight: 700;
}
/* full-page film grain */
body::after {
content: '';
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
opacity: 0.06;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
}
html:not(.theme-paper) body::after,
.dark-mode body::after {
mix-blend-mode: screen;
opacity: 0.045;
opacity: 0.04;
}
/* quick page transition */
.page-enter-active,
.page-leave-active {
transition: opacity 0.18s ease;
a {
color: var(--accent);
text-decoration: none;
text-underline-offset: 3px;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
a:hover {
text-decoration: underline;
}
.basic-link {
color: var(--text);
transition: opacity 0.2s ease;
text-underline-offset: 4px;
}
.basic-link:hover {
opacity: 0.5;
}
.punch-link {
display: inline-block;
vertical-align: bottom;
article {
position: relative;
color: var(--color-white-500);
text-decoration: none;
transition: all 200ms ease;
padding: 0 4px;
margin: 0 -4px;
background-image: linear-gradient(
var(--color-white-500),
var(--color-white-500)
);
background-size: 100% 1px;
background-position: bottom;
background-repeat: no-repeat;
mix-blend-mode: screen;
width: var(--col);
margin-left: var(--col-left);
margin-right: auto;
overflow-wrap: break-word;
}
.punch-link:hover {
background-size: 100% 100%;
background-image: linear-gradient(#fff, #fff);
color: #000;
article h2,
article h3 {
font-weight: 400;
font-style: italic;
color: var(--muted);
font-size: 1rem;
margin: 2.25rem 0 0.75rem;
line-height: 1.3;
}
.back-link {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: small;
article > h3:first-child {
margin-top: 0;
}
article p {
margin: 0 0 1em;
}
article ul,
article ol {
margin: 0 0 1em;
padding-left: 1.2em;
}
article ul {
list-style-type: '– ';
}
article li {
margin: 0 0 0.35em;
}
article li p {
margin: 0;
}
article hr {
margin: 2.5rem 0;
border: none;
border-top: 1px solid var(--hairline);
}
article blockquote {
--hand-w: 2.35rem;
--hand-h: calc(var(--hand-w) * 44 / 100);
position: relative;
margin: 1.5rem 0;
padding: 0.15rem 0 0.15rem 3.15rem;
border: 0;
font-style: italic;
font-weight: 400;
font-size: 1.08em;
line-height: 1.55;
color: var(--text);
text-decoration: none;
transition: opacity 0.2s ease;
}
.back-link .arrow {
transition: transform 0.2s ease;
article blockquote p:last-child {
margin-bottom: 0;
}
.back-link:hover .arrow {
transform: translateX(-4px);
article blockquote::before {
content: '';
position: absolute;
top: 0.35em;
left: 0;
width: var(--hand-w);
height: var(--hand-h);
background: url('~/assets/images/L.png') center / contain no-repeat;
pointer-events: none;
}
html:not(.theme-paper) article blockquote::before,
.dark-mode article blockquote::before {
filter: invert(1);
}
article img {
max-width: 100%;
height: auto;
border-radius: var(--radius);
background: var(--img-matte);
}
article p:has(> img) {
margin: 1.25rem 0;
}
article .article-body > :first-child {
margin-top: 0;
}
code {
font-family: var(--font-mono);
font-size: 0.86em;
color: var(--text);
background: var(--code-bg);
padding: 0.12em 0.35em;
border-radius: 3px;
}
pre {
margin: 0 0 1em;
padding: 0.9rem 1rem;
color: var(--text);
background: var(--bg-secondary);
border-radius: var(--radius);
overflow-x: auto;
line-height: 1.55;
}
pre code {
padding: 0;
color: inherit;
background: none;
font-size: 0.8rem;
}
pre.shiki {
background: var(--bg-secondary);
}
html.light-mode .shiki span {
color: var(--shiki-light-mode, var(--shiki-default));
background: transparent;
}
html.dark-mode .shiki span {
color: var(--shiki-dark-mode, var(--text));
background: transparent;
}
@media (max-width: 640px) {
body {
font-size: 17px;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
<script setup lang="ts">
defineProps<{
pages: Array<{
path: string;
title: string;
date: string | Date;
}>;
}>();
const formatDate = (dateString: string | Date) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
</script>
<template>
<template v-if="pages.length">
<NuxtLink
v-for="page in pages"
:key="page.path"
:to="'/writing' + page.path"
class="listing"
>
<span class="listing__title">{{ page.title }}</span>
<span class="listing__rule" aria-hidden="true"></span>
<time :datetime="new Date(page.date).toISOString()">{{
formatDate(page.date)
}}</time>
</NuxtLink>
</template>
<p v-else class="empty-state">No thoughts...head empty</p>
</template>
<style scoped>
.listing {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin: 0 0 0.65rem;
text-decoration: none;
color: inherit;
}
.listing:hover {
text-decoration: none;
}
.listing:hover .listing__title {
text-decoration: underline;
text-underline-offset: 3px;
}
.listing__title {
flex-shrink: 0;
color: var(--text);
}
.listing__rule {
flex: 1 1 auto;
min-width: 1.25rem;
border-bottom: 1px solid var(--hairline);
transform: translateY(-0.35em);
}
.listing time {
flex-shrink: 0;
font-family: var(--font-plain);
font-size: 0.75rem;
font-style: italic;
color: var(--muted);
white-space: nowrap;
}
.empty-state {
color: var(--muted);
font-style: italic;
}
@media (max-width: 640px) {
.listing {
flex-direction: column;
gap: 0.15rem;
}
.listing__rule {
display: none;
}
}
</style>
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { watchMarginNotes } from '~/utils/stackMarginNotes';
onMounted(() => {
watchMarginNotes();
});
</script>
<template>
<span class="margin-note-root">
<span class="margin-note-mark" aria-hidden="true"></span>
<span class="margin-note"><slot /></span>
</span>
</template>
<style scoped>
.margin-note-root {
display: inline;
}
.margin-note-mark {
display: inline;
}
.margin-note {
display: block;
margin: 0.4rem 0 1rem;
font-size: 0.85rem;
font-style: italic;
color: var(--muted);
line-height: 1.45;
}
@media (min-width: 1200px) {
.margin-note {
position: absolute;
left: calc(100% + var(--note-gap));
width: var(--note-width);
margin: 0;
font-size: 0.8rem;
}
}
</style>
+50
View File
@@ -0,0 +1,50 @@
<script setup lang="ts">
const props = defineProps<{
bleed?: boolean | string;
}>();
const isBleed = computed(
() =>
props.bleed === true ||
props.bleed === '' ||
props.bleed === 'true',
);
</script>
<template>
<div class="notice" :class="{ 'notice--bleed': isBleed }">
<slot />
</div>
</template>
<style scoped>
.notice {
background: var(--navy);
color: #f6f7f9;
padding: 1rem 1.15rem;
margin: 1.5rem 0;
border-radius: var(--radius);
}
.notice :deep(a) {
color: var(--accent-soft);
}
.notice :deep(a:hover) {
color: #fff;
}
.notice :deep(p:last-child) {
margin-bottom: 0;
}
.notice--bleed {
box-sizing: border-box;
width: 100vw;
max-width: 100vw;
margin: 1.5rem 0 1.5rem calc(-1 * var(--col-left));
padding: 1.15rem var(--col-left);
padding-right: calc(100vw - var(--col-left) - var(--col));
border-radius: 0;
}
</style>
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
import {
redactionFamily,
resolveRedactionStrength,
} from '~/utils/fonts';
const props = withDefaults(
defineProps<{
strength?: number | string;
}>(),
{
strength: 100,
},
);
const grade = computed(() => resolveRedactionStrength(props.strength));
const family = computed(
() => `'${redactionFamily(grade.value)}', Georgia, serif`,
);
</script>
<template>
<span
class="redacted"
:data-strength="grade"
:style="{ fontFamily: family }"
><slot /></span>
</template>
<style scoped>
.redacted {
font-weight: 400;
}
:where(strong, b) .redacted {
font-weight: 700;
}
</style>
+41
View File
@@ -0,0 +1,41 @@
import {
BG_THEMES,
BG_THEME_STORAGE_KEY,
DEFAULT_BG_THEME,
isBgThemeId,
type BgThemeId,
} from '~/utils/themes';
export function useBgTheme() {
const id = useState<BgThemeId>('bg-theme', () => DEFAULT_BG_THEME);
const colorMode = useColorMode();
const current = computed(
() => BG_THEMES.find((theme) => theme.id === id.value) ?? BG_THEMES[0],
);
const applyClass = (next: BgThemeId) => {
if (!import.meta.client) return;
for (const theme of BG_THEMES) {
document.documentElement.classList.remove(`theme-${theme.id}`);
}
document.documentElement.classList.add(`theme-${next}`);
colorMode.preference = next === 'paper' ? 'light' : 'dark';
};
const setTheme = (next: BgThemeId) => {
id.value = next;
applyClass(next);
if (import.meta.client) {
localStorage.setItem(BG_THEME_STORAGE_KEY, next);
}
};
onMounted(() => {
const saved = localStorage.getItem(BG_THEME_STORAGE_KEY);
const next = isBgThemeId(saved) ? saved : id.value;
setTheme(next);
});
return { id, current, setTheme, themes: BG_THEMES };
}
+35
View File
@@ -0,0 +1,35 @@
import {
BODY_FONTS,
BODY_FONT_STORAGE_KEY,
DEFAULT_BODY_FONT,
isBodyFontId,
type BodyFontId,
} from '~/utils/fonts';
export function useBodyFont() {
const id = useState<BodyFontId>('body-font', () => DEFAULT_BODY_FONT);
const applyClass = (next: BodyFontId) => {
if (!import.meta.client) return;
for (const font of BODY_FONTS) {
document.documentElement.classList.remove(`font-${font.id}`);
}
document.documentElement.classList.add(`font-${next}`);
};
const setFont = (next: BodyFontId) => {
id.value = next;
applyClass(next);
if (import.meta.client) {
localStorage.setItem(BODY_FONT_STORAGE_KEY, next);
}
};
onMounted(() => {
const saved = localStorage.getItem(BODY_FONT_STORAGE_KEY);
const next = isBodyFontId(saved) ? saved : id.value;
setFont(next);
});
return { id, setFont, fonts: BODY_FONTS };
}
+9 -62
View File
@@ -9,68 +9,15 @@ useHead({
</script>
<template>
<div class="error-page">
<svg width="0" height="0" aria-hidden="true" style="position: absolute">
<filter id="grainy" x="0" y="0" width="100%" height="100%">
<feTurbulence
type="fractalNoise"
baseFrequency=".85"
numOctaves="3"
></feTurbulence>
<feColorMatrix type="saturate" values="0"></feColorMatrix>
<feBlend mode="multiply" in="SourceGraphic"></feBlend>
</filter>
</svg>
<div class="error-card">
<div class="error-content">
<h1>{{ error.status }}</h1>
<p>
{{ error.message ?? 'Sorry, something went wrong.' }}
</p>
<p>
<NuxtLink to="/" class="punch-link"
>Back to the homepage.</NuxtLink
<NuxtLayout
name="main"
:title="String(props.error.statusCode ?? 500)"
:description="props.error.message ?? 'Sorry, something went wrong.'"
>
<article>
<p>
<NuxtLink to="/">Back to the homepage.</NuxtLink>
</p>
</div>
</div>
</div>
</article>
</NuxtLayout>
</template>
<style scoped>
.error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
}
.error-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 16rem;
color: var(--bg);
isolation: isolate;
}
.error-card::before {
content: '';
position: absolute;
inset: 0;
background: url('~/assets/images/blend.jpeg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: url(#grainy);
z-index: -1;
}
.error-content {
color: var(--text);
}
</style>
+241 -253
View File
@@ -1,313 +1,301 @@
<script lang="ts">
// module-scoped so it survives client-side navigation —
// the intro should only ever play once per visit
let introPlayed = false;
</script>
<script setup lang="ts">
defineProps<{
withDefaults(
defineProps<{
title?: string;
description?: string;
}>();
}>(),
{
title: "Hey, I'm Jaron.",
description: 'Software engineer by trade — builder by nature.',
},
);
const theme = useColorMode();
const { id, themes, setTheme } = useBgTheme();
const { id: fontId, fonts, setFont } = useBodyFont();
const toggleTheme = () => {
theme.preference = theme.preference === 'light' ? 'dark' : 'light';
const onThemeChange = (event: Event) => {
const value = (event.target as HTMLSelectElement).value;
const next = themes.find((theme) => theme.id === value);
if (next) setTheme(next.id);
};
const { $gsap } = useNuxtApp();
const pageEl = useTemplateRef('pageEl');
const initialized = ref(false);
onMounted(() => {
initialized.value = true;
if (!$gsap || !pageEl.value || introPlayed) return;
introPlayed = true;
const reduced = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
const ctx = $gsap.context(() => {
if (reduced) return;
const tl = $gsap.timeline({
defaults: { ease: 'power3.out' },
});
tl.from('.header-lower', {
autoAlpha: 0,
y: 12,
duration: 0.9,
ease: 'power2.out',
})
.from('.name', { yPercent: 110, duration: 0.9 }, 0.2)
.from(
'.title',
{ yPercent: 120, autoAlpha: 0, duration: 0.8 },
0.35,
)
.from(
['.footer-upper', '.footer-lower'],
{ autoAlpha: 0, duration: 0.7, ease: 'power2.out' },
0.55,
)
.from('.header-upper', { autoAlpha: 0, duration: 0.7 }, 0.55);
tl.from(
'main',
{ autoAlpha: 0, y: 16, duration: 0.8, ease: 'power2.out' },
0.4,
);
}, pageEl.value);
onBeforeUnmount(() => ctx.revert());
});
const onFontChange = (event: Event) => {
const value = (event.target as HTMLSelectElement).value;
const next = fonts.find((font) => font.id === value);
if (next) setFont(next.id);
};
</script>
<template>
<div v-show="initialized">
<svg width="0" height="0" aria-hidden="true" style="position: absolute">
<filter id="grainy" x="0" y="0" width="100%" height="100%">
<feTurbulence
type="fractalNoise"
baseFrequency=".85"
numOctaves="3"
></feTurbulence>
<feColorMatrix type="saturate" values="0"></feColorMatrix>
<feBlend mode="multiply" in="SourceGraphic"></feBlend>
</filter>
</svg>
<div class="page-wrapper" ref="pageEl">
<header class="header">
<div class="header-upper">
<slot name="header-upper">
<div class="flex-center header-upper-nav">
<button
class="theme-toggle"
@click="toggleTheme"
v-if="!theme.unknown"
>
{{ theme.value === 'dark' ? '🌚' : '🌝' }}
</button>
<div class="flex-ff"></div>
<a
class="basic-link"
href="https://github.com/jaronpate"
>
GitHub
</a>
<NuxtLink to="/writing" class="basic-link">
Writing
<div class="site">
<header class="hero">
<div class="hero__inner">
<div class="hero__nav">
<NuxtLink to="/" class="wordmark" aria-label="Home">
@jaronpate
</NuxtLink>
<nav class="site-nav">
<NuxtLink to="/writing">Writing</NuxtLink>
<a href="https://github.com/jaronpate">GitHub</a>
<label class="theme-picker">
<span class="visually-hidden">Background</span>
<select
class="theme-select"
:value="id"
@change="onThemeChange"
>
<option
v-for="theme in themes"
:key="theme.id"
:value="theme.id"
>
{{ theme.label }}
</option>
</select>
</label>
<!-- <label class="theme-picker">
<span class="visually-hidden">Font</span>
<select
class="theme-select"
:value="fontId"
@change="onFontChange"
>
<option
v-for="font in fonts"
:key="font.id"
:value="font.id"
>
{{ font.label }}
</option>
</select>
</label> -->
</nav>
</div>
</slot>
</div>
<div class="header-lower">
<slot name="header-lower">
<div class="mask">
<h1 class="name">
{{ title || "Hey, I'm Jaron" }}
</h1>
</div>
<div class="mask">
<p class="title">
{{
description ||
'Software Engineer by trade — builder by nature.'
}}
</p>
</div>
</slot>
<h1 class="hero__title">{{ title }}</h1>
<p v-if="description" class="hero__sub">{{ description }}</p>
</div>
</header>
<main>
<div class="page-body">
<slot />
<div style="flex: 1"></div>
</main>
</div>
<footer class="footer">
<div class="footer-upper flex-center">
<p>
<a class="punch-link" href="mailto:yo@jp.wtf"
>Say hi.</a
>
</p>
<footer class="site-footer">
<a href="mailto:yo@jp.wtf">yo@jp.wtf</a>
<div class="flex-ff"></div>
<p style="text-align: right">Let's make something cool.</p>
</div>
<div class="footer-lower flex-center" style="gap: var(--gap)">
<p style="text-align: right">Est. 2003</p>
<!-- <p style="font-family: monospace">🐟🎩.dev</p> -->
<div class="flex-ff"></div>
<p style="text-align: right">Austin, TX</p>
</div>
<span>Est. 2003</span>
<span>Austin, TX</span>
</footer>
</div>
</div>
</template>
<style scoped>
.page-wrapper {
--main-width: 50vw;
width: var(--main-width);
margin: 0 auto;
display: flex;
flex-direction: column;
/*gap: 1.5rem;*/
.site {
width: 100%;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
}
main {
.page-body {
flex: 1;
padding-top: 2.25rem;
}
.hero {
--hero-text: #fbfbf7;
position: relative;
isolation: isolate;
color: var(--hero-text);
padding: 1.25rem 0 3rem;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
background:
linear-gradient(rgba(12, 12, 18, 0.1), rgba(12, 12, 18, 0.18)),
url('~/assets/images/blend.jpeg') center / cover no-repeat;
}
.hero__inner {
width: var(--col);
margin-left: var(--col-left);
}
.hero__nav {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
margin-bottom: 2.75rem;
font-family: var(--font-plain);
font-size: 0.8rem;
}
svg[height='0'] {
position: fixed;
.wordmark,
.site-nav a,
.theme-select {
color: var(--hero-text);
}
.mask {
overflow: hidden;
}
.name {
font-size: 3rem;
.wordmark {
display: flex;
align-items: center;
gap: 0.7rem;
text-decoration: none;
font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 400;
line-height: 1.2;
margin: 0;
line-height: 1;
}
.title {
font-weight: 300;
margin: 0;
font-style: italic;
color: var(--color-white-500);
.wordmark:hover,
.wordmark.router-link-active {
color: var(--hero-text);
text-decoration: none;
opacity: 0.75;
}
.header {
.site-nav {
display: flex;
flex-direction: column;
}
.footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.header-upper {
height: 8vh;
}
.header-upper-nav {
padding: 0 2rem;
height: 100%;
font-size: small;
align-items: center;
gap: 1rem;
margin-left: auto;
}
.header-lower {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 16px;
padding: 3rem 2.5rem;
color: var(--color-white-500);
border-radius: 16px;
overflow: hidden;
position: relative;
isolation: isolate;
.site-nav a:hover,
.site-nav a.router-link-active {
color: var(--hero-text);
opacity: 0.7;
}
.header-lower::before {
content: '';
position: absolute;
inset: 0;
background: url('~/assets/images/blend.jpeg');
background-size: cover;
background-position: left top;
.theme-picker {
display: inline-flex;
align-items: center;
}
.theme-select {
appearance: none;
background-color: rgba(255, 255, 255, 0.14);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath fill='%23fbfbf7' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
filter: url(#grainy);
border-radius: 16px;
z-index: -1;
}
.footer-upper {
padding: 3.25rem 2.5rem;
color: var(--color-white-500);
border-radius: 16px;
overflow: hidden;
position: relative;
isolation: isolate;
}
.footer-upper::before {
content: '';
position: absolute;
inset: 0;
background: url('~/assets/images/blend.jpeg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: url(#grainy);
border-radius: 16px;
z-index: -1;
}
.footer-lower {
padding: 2rem;
height: 8vh;
font-size: small;
gap: 1rem;
}
.theme-toggle {
background: transparent;
border: none;
background-position: right 0.55em center;
background-size: 0.5em;
border: 1px solid rgba(251, 251, 247, 0.35);
border-radius: 4px;
cursor: pointer;
font-size: large;
font-weight: bold;
color: var(--text);
transition: opacity 0.2s ease;
font-family: var(--font-plain);
font-size: 0.8rem;
line-height: 1;
padding: 0.35em 1.55em 0.35em 0.65em;
}
.theme-select:hover {
background-color: rgba(255, 255, 255, 0.22);
}
.theme-select option {
color: #1a1a1a;
background: #fff;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.theme-toggle:hover {
opacity: 0.5;
}
.footer p {
.hero__title {
margin: 0;
font-size: 2.75rem;
font-weight: 400;
line-height: 1.15;
}
@media (max-width: 1024px) {
.page-wrapper {
--main-width: 80vw;
}
.hero__sub {
margin: 0.6rem 0 0;
font-weight: var(--body-weight);
font-style: italic;
font-size: 1.05rem;
color: rgba(251, 251, 247, 0.88);
}
@media (max-width: 768px) {
.page-wrapper {
--main-width: 100vw;
padding: 0 1rem;
.site-footer {
--flare-length: 1.125rem;
--flare-spread: 1.125rem;
--flare-overhang: 1.5rem;
position: relative;
display: flex;
align-items: baseline;
gap: 1rem;
width: var(--col);
margin-top: 2rem;
margin-left: var(--col-left);
margin-right: auto;
padding: calc(var(--flare-spread) + 0.45rem) 0 0.85rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted);
}
.site-footer::before {
content: '';
position: absolute;
top: 0;
left: calc(-1 * var(--flare-overhang));
right: calc(-1 * var(--flare-overhang));
height: var(--flare-spread);
background-color: var(--muted);
pointer-events: none;
-webkit-mask:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
left center / var(--flare-length) 100% no-repeat,
linear-gradient(#000, #000) center /
calc(100% - var(--flare-length) * 1.55) 1px no-repeat,
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
right center / var(--flare-length) 100% no-repeat;
mask:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
left center / var(--flare-length) 100% no-repeat,
linear-gradient(#000, #000) center /
calc(100% - var(--flare-length) * 1.55) 1px no-repeat,
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
right center / var(--flare-length) 100% no-repeat;
}
.site-footer a {
color: var(--muted);
}
.site-footer a:hover {
color: var(--accent);
}
.flex-ff {
flex: 1;
}
@media (max-width: 640px) {
.hero {
padding-bottom: 2.25rem;
}
.header-lower,
.footer-upper {
border-radius: 12px;
.hero__nav {
flex-wrap: wrap;
margin-bottom: 1.75rem;
}
.header-lower::before,
.footer-upper::before {
border-radius: 12px;
.hero__title {
font-size: 2rem;
}
}
</style>
+44 -411
View File
@@ -1,10 +1,4 @@
<script lang="ts">
// module-scoped so the reveal only plays once per visit
let sectionsRevealed = false;
</script>
<script setup lang="ts">
import { ref } from 'vue';
import {
DEFAULT_DESCRIPTION,
DEFAULT_IMAGE,
@@ -36,19 +30,11 @@ useHead({
link: [{ rel: 'canonical', href: SITE_URL }],
});
const titles = [
"Hey, I'm Jaron.",
'Nice to meet you.',
'Welcome.',
'Willkommen.',
'Bienvenue.',
];
const selectedWorks = [
{
title: 'Edlink',
description:
"I built many things I'm proud of at Edlink. Notably, I designed and shipped Edlink’s internal billing platform for ~150 clients, a flat file ingestion engine that enabled arbitrary data mapping, client-embedded UI widgets, user onboarding flows, and a secure sandbox for running untrusted code with a live data-preview service.",
"I built many things I'm proud of at Edlink. Notably, I designed and shipped the internal billing platform for ~150 clients, a flat file ingestion engine that enabled importing of arbitrary data, client-embedded UI widgets, user onboarding flows, and a secure sandbox for running untrusted code with a live data-preview service.",
link: 'https://ed.link',
images: [
{
@@ -186,117 +172,20 @@ useHead({
],
});
const photos = [
{
src: '/photography/athens_red_scooter.jpg',
alt: 'Red scooter in Athens',
location: 'Athens',
year: '2026',
},
{
src: '/photography/athens_subway.jpg',
alt: 'Athens metro station',
location: 'Athens',
year: '2026',
},
{
src: '/photography/athens_sunset.jpg',
alt: 'Athens at sunset',
location: 'Athens',
year: '2026',
},
{
src: '/photography/london_porsche.jpg',
alt: 'Porsche shop in London',
location: 'London',
year: '2025',
},
{
src: '/photography/underground.jpg',
alt: 'Entrace to the London underground',
location: 'London',
year: '2026',
},
{
src: '/photography/austin_parking.jpg',
alt: 'Parking garage in Austin',
location: 'Austin',
year: '2024',
},
];
const pageTitle = ref(titles[0]);
const rotateTitle = () => {
setTimeout(() => {
const i = titles.findIndex((t) => t === pageTitle.value);
pageTitle.value = titles[i + 1] ?? titles[0] ?? 'Oops!';
rotateTitle();
}, 2000);
};
definePageMeta({
layout: false,
});
const { $gsap } = useNuxtApp();
const pageRoot = ref<HTMLElement | null>(null);
onMounted(() => {
rotateTitle();
useHead({
title: pageTitle,
});
if (!$gsap || !pageRoot.value || sectionsRevealed) return;
sectionsRevealed = true;
const reduced = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
const ctx = $gsap.context(() => {
if (reduced) return;
// sections in view on load are revealed by the layout's intro;
// below-the-fold sections rise in as they enter the viewport
$gsap.utils.toArray<HTMLElement>('main section').forEach((el) => {
const inView = el.getBoundingClientRect().top < window.innerHeight;
if (inView) return;
$gsap.from(el, {
autoAlpha: 0,
y: 24,
duration: 0.6,
ease: 'power2.out',
scrollTrigger: {
trigger: el,
start: 'top 92%',
},
});
});
}, pageRoot.value);
onBeforeUnmount(() => ctx.revert());
});
</script>
<template>
<div ref="pageRoot">
<NuxtLayout name="main">
<section class="rows">
<h5>Selected Work</h5>
<div class="section__content">
<a
v-for="work in selectedWorks"
:key="work.title"
class="work"
:href="work.link"
target="_blank"
>
<h3 class="work__title">{{ work.title }}</h3>
<p class="work__description">
<article>
<h3>Selected Work</h3>
<template v-for="work in selectedWorks" :key="work.title">
<p>
<a :href="work.link" target="_blank" rel="noopener">{{
work.title
}}</a>:
{{ work.description }}
</p>
<div class="work__images">
@@ -308,34 +197,26 @@ onMounted(() => {
<img :src="image.src" :alt="image.alt" />
</figure>
</div>
</a>
</div>
</section>
<section class="rows">
<h5>Projects</h5>
<div class="section__content">
<a
v-for="project in projects"
:key="project.href"
:href="project.href"
target="_blank"
class="project"
>
<h3 class="project__title">{{ project.title }}</h3>
<p class="project__description">
</template>
<h3>Projects</h3>
<ul>
<li v-for="project in projects" :key="project.href">
<p>
<a :href="project.href" target="_blank" rel="noopener">{{
project.title
}}</a>:
{{ project.description }}
</p>
</a>
</div>
</section>
<section class="about">
<h5>About</h5>
<div class="section__content">
</li>
</ul>
<h3>About</h3>
<p>
I left school, my job, and moved to Austin in 2022 to write
code full time. That was definitely one of the best and
most life changing decisions I've ever made. I've learned
a lot since then and <em>still</em> have much more to learn.
I left school, my job, and moved to Austin in 2022 to write code
full time. That was definitely one of the best and most life
changing decisions I've ever made. I've learned a lot since then
and <em>still</em> have much more to learn.
</p>
<p>
These days I build things because I find joy in making something
@@ -343,155 +224,64 @@ onMounted(() => {
level of polish that users don't even notice.
</p>
<p>
In a time where code is cheap I pride myself in taste and execution.
The best sticking point — in my limited opinion so far — is the
way using your product <em>feels</em>. So I want everything I make to
simply <em>feel</em> the best to use.
In a time where code is cheap I pride myself in taste and
execution. The best sticking point (in my limited opinion so far)
is the way using your product <em>feels</em>. So I want
everything I make to simply <em>feel</em> the best to use.
</p>
<p>
If you want to build something cool or connect
<a class="basic-link" href="mailto:yo@jp.wtf">hit me up</a>
— we'll grab a coffee or beer if you're in Austin! I
still dont know enough people to form a band yet ;)
<a href="mailto:yo@jp.wtf">hit me up</a> — we'll grab a coffee or
beer if you're in Austin! I still dont know enough people to form
a band yet ;)
</p>
</div>
</section>
<!-- <section class="photos">
<h5>Photos</h5>
<div class="section__content">
<div class="photo-grid">
<div
v-for="photo in photos"
:key="photo.src"
class="photo-grid__item"
>
<div class="photo__image">
<img :src="photo.src" :alt="photo.alt" />
</div>
<div class="photo__overlay">
<p>{{ photo.location }}</p>
<p>{{ photo.year }}</p>
</div>
</div>
</div>
</div>
</section> -->
</article>
</NuxtLayout>
</div>
</template>
<style scoped>
section {
margin-top: calc(var(--gap) * 2);
}
section > .section__content {
padding: 0 calc(var(--gap) * 2);
}
/* section:not(:has(h5)) > .section__content {
padding: 2rem;
} */
section > h5 {
--flare-length: 1.125rem; /* how far the flare extends horizontally */
--flare-spread: 1.125rem; /* how far the arms open up/down */
font-weight: 400;
font-style: italic;
color: var(--muted);
display: flex;
align-items: center;
gap: 1rem;
margin: 0 0 var(--gap) 0;
line-height: 1;
}
section > h5::before {
content: '';
width: var(--flare-length);
height: var(--flare-spread);
flex-shrink: 0;
background-color: var(--muted);
/* Optically center */
transform: translateY(-0.12em);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
center / 100% 100% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
center / 100% 100% no-repeat;
}
section > h5::after {
content: '';
height: var(--flare-spread);
flex-grow: 1;
background-color: var(--muted);
/* Optically center */
transform: translateY(-0.12em);
-webkit-mask:
linear-gradient(#000, #000) left center /
calc(100% - var(--flare-length) * 0.45) 1px no-repeat,
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
right center / var(--flare-length) 100% no-repeat;
mask:
linear-gradient(#000, #000) left center /
calc(100% - var(--flare-length) * 0.45) 1px no-repeat,
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
right center / var(--flare-length) 100% no-repeat;
}
section:last-of-type {
margin-bottom: calc(var(--gap) * 2);
}
.work {
}
.work__images {
/*display: flex;
flex-wrap: no-wrap;
align-items: center;
justify-content: center;*/
--dot-color: #666;
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: var(--gap);
overflow-x: auto;
overflow-y: hidden;
margin-top: var(--gap);
margin: 1.25rem 0 1.75rem;
padding-bottom: 0.35rem;
}
.work__image {
--dot-size: 6%;
margin: 0;
/*padding: calc(var(--gap) * 2) var(--gap) var(--gap);*/
padding: var(--gap);
flex-shrink: 0;
border-radius: var(--radius);
background-color: var(--bg-secondary);
background-image: radial-gradient(
circle at 4px 4px,
var(--dot-color) var(--dot-size),
transparent calc(var(--dot-size) + 1%)
);
background-position: 4px 4px;
background-size: 12px 12px;
background-repeat: repeat;
background-color: var(--bg-secondary);
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--gap);
}
.work__image img {
border-radius: var(--radius);
object-fit: cover;
max-width: none;
width: auto;
height: auto;
max-height: 350px;
object-fit: cover;
border-radius: var(--radius);
background: none;
}
@media screen and (max-width: 750px) {
@@ -499,161 +289,4 @@ section:last-of-type {
max-height: 300px;
}
}
.work__image figcaption {
font-size: 0.6rem;
line-height: 1;
font-family: var(--font-plain);
background: var(--bg-secondary);
border-radius: 2px;
padding: 3px 4px;
background: var(--dot-color);
/*border: 1px solid var(--dot-color);*/
display: flex;
align-items: center;
justify-content: center;
}
.rows > .section__content {
display: flex;
flex-direction: column;
gap: var(--gap);
}
.project,
.work {
/*border-bottom: 1px solid var(--muted);*/
/*padding: var(--gap) 0;*/
display: block;
text-decoration: none;
color: inherit;
}
.work {
border: 0;
}
.project:first-of-type,
.work:first-of-type {
padding-top: 0;
}
.project:last-of-type,
.work:last-of-type {
padding-bottom: 0;
}
.project:last-child {
border-bottom: none;
}
.project:hover > .project__title {
text-decoration: underline;
text-underline-offset: 4px;
font-style: italic;
font-family: var(--font-hover);
}
.project__title,
.work__title {
margin: 0;
font-weight: 400;
margin-bottom: calc(var(--gap) * 0.5);
}
.project__title::after,
.work__title::after {
content: '↗';
position: relative;
display: inline-block;
margin-left: 0.4em;
font-size: 0.75em;
opacity: 0;
vertical-align: center;
transform: translate(-6px, 6px);
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
.project:hover .project__title::after,
.work:hover .work__title::after {
opacity: 1;
transform: translate(0, 0);
}
.project__description,
.work__description {
margin: 0;
font-size: small;
font-weight: 400;
font-style: italic;
color: var(--muted);
}
.photo-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.photo-grid__item {
cursor: pointer;
position: relative;
aspect-ratio: 1 / 1;
}
.photo-grid__item:hover > .photo__overlay {
opacity: 1;
}
.photo__image {
width: 100%;
height: 100%;
}
.photo__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo__overlay {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
color: var(--color-white-500);
z-index: 2;
transition: opacity 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.photo__overlay p {
font-weight: 500;
margin: 0;
}
.photo__overlay p:nth-child(2) {
font-weight: 300;
font-style: italic;
font-size: small;
}
.about p:last-child {
margin: 0;
}
@media (max-width: 768px) {
.photo-grid {
grid-template-columns: 1fr;
}
}
</style>
+81 -56
View File
@@ -12,40 +12,76 @@ const articleUrl = `${SITE_URL}${route.path}`;
const webpageId = `${articleUrl}#webpage`;
const articleId = `${articleUrl}#article`;
const blogId = `${SITE_URL}/writing#blog`;
const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('writing')
.path(route.path.replace('/writing/', ''))
.first();
});
const slug = computed(() =>
Array.isArray(route.params.slug)
? route.params.slug.join('/')
: String(route.params.slug ?? ''),
);
const contentPath = computed(() => '/' + slug.value);
const folderName = computed(
() => slug.value.split('/').filter(Boolean).at(-1) ?? slug.value,
);
if (!page.value) {
const { data } = await useAsyncData(
() => `writing:${route.path}`,
async () => {
const all = await queryCollection('writing').order('date', 'DESC').all();
const visible = all.filter((page) => import.meta.dev || !page.demo);
const article = visible.find(
(page) =>
page.path === contentPath.value || page.stem === slug.value,
);
if (article) {
return { kind: 'article' as const, article, pages: [] };
}
const prefix = contentPath.value.endsWith('/')
? contentPath.value
: `${contentPath.value}/`;
const pages = visible.filter((page) => page.path.startsWith(prefix));
if (!pages.length) return null;
return { kind: 'folder' as const, article: null, pages };
},
{ watch: [() => route.path] },
);
if (!data.value) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' });
}
const articleTitle = page.value.title;
const articleDescription = page.value.description;
const page = computed(() => data.value?.article ?? null);
const isFolder = computed(() => data.value?.kind === 'folder');
const articleTitle = page.value?.title ?? folderName.value;
const articleDescription = page.value?.description ?? '';
useSeoMeta({
title: articleTitle,
ogTitle: articleTitle,
description: articleDescription,
ogDescription: articleDescription,
description: articleDescription || articleTitle,
ogDescription: articleDescription || articleTitle,
ogUrl: articleUrl,
ogType: 'article',
ogType: isFolder.value ? 'website' : 'article',
ogImage: DEFAULT_IMAGE,
ogImageAlt: articleTitle,
twitterCard: 'summary',
twitterTitle: articleTitle,
twitterDescription: articleDescription,
twitterDescription: articleDescription || articleTitle,
twitterImage: DEFAULT_IMAGE,
articlePublishedTime: new Date(page.value.date).toISOString(),
articleModifiedTime: new Date(page.value.date).toISOString(),
articleAuthor: [SITE_NAME],
articlePublishedTime: page.value
? new Date(page.value.date).toISOString()
: undefined,
articleModifiedTime: page.value
? new Date(page.value.date).toISOString()
: undefined,
articleAuthor: page.value ? [SITE_NAME] : undefined,
});
useHead({
link: [{ rel: 'canonical', href: articleUrl }],
script: [
script: page.value
? [
{
key: 'json-ld-article',
type: 'application/ld+json',
@@ -82,8 +118,12 @@ useHead({
url: articleUrl,
headline: articleTitle,
description: articleDescription,
datePublished: new Date(page.value.date).toISOString(),
dateModified: new Date(page.value.date).toISOString(),
datePublished: new Date(
page.value.date,
).toISOString(),
dateModified: new Date(
page.value.date,
).toISOString(),
image: {
'@type': 'ImageObject',
'@id': `${articleUrl}#article-image`,
@@ -131,55 +171,40 @@ useHead({
],
}),
},
],
]
: [],
});
const formatDate = (date: Date | string) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
definePageMeta({
layout: false,
});
</script>
<template>
<div>
<NuxtLayout
name="main"
:title="page?.title"
:description="page?.description"
:title="page ? page.title : folderName"
:description="page ? formatDate(page.date) : `Writing from ${folderName}`"
>
<template #header-upper>
<div class="header-upper-inner">
<NuxtLink
to="/writing"
class="back-link"
aria-label="Back to writing"
>
<span class="arrow">←</span>
<span>Back</span>
</NuxtLink>
<article v-if="page">
<div class="article-body">
<p v-if="page.description">
<strong>{{ page.description }}</strong>
</p>
<ContentRenderer :value="page" />
</div>
</template>
<ContentRenderer
class="writing__content"
v-if="page"
:value="page"
/>
</article>
<article v-else>
<WritingList :pages="data?.pages ?? []" />
</article>
</NuxtLayout>
</div>
</template>
<style scoped>
.header-upper-inner {
display: flex;
align-items: center;
height: 100%;
padding: 0 2rem;
}
.writing__content {
padding: 2rem;
}
.writing__content :deep(> :first-child) {
margin-top: 0;
}
</style>
+10 -117
View File
@@ -7,9 +7,11 @@ import {
websiteId,
} from '~/utils/site';
// const { data: pages } = await useAsyncData('writing-pages', () =>
// queryCollection('writing').order('date', 'DESC').all(),
// );
// TODO: Uncomment when ready to show writing
// const { data: pages } = await useAsyncData('writing-pages', async () => {
// const all = await queryCollection('writing').order('date', 'DESC').all();
// return import.meta.dev ? all : all.filter((page) => !page.demo);
// });
const pages = ref<any[]>([]);
@@ -74,7 +76,7 @@ useHead({
name: SITE_NAME,
},
blogPost: (pages.value ?? []).map((page) => ({
'@id': `${writingUrl}/${page.path}#article`,
'@id': `${writingUrl}${page.path}#article`,
})),
},
{
@@ -85,7 +87,7 @@ useHead({
(page, index) => ({
'@type': 'ListItem',
position: index + 1,
url: `${writingUrl}/${page.path}`,
url: `${writingUrl}${page.path}`,
name: page.title,
}),
),
@@ -114,128 +116,19 @@ useHead({
],
});
const formatDate = (dateString: string) => {
const date = new Date(dateString);
// ex. May 5, 2026
return `${date.toLocaleDateString('en-US', { month: 'short' })} ${date.getDate()}, ${date.getFullYear()}`;
};
definePageMeta({
layout: false,
});
</script>
<template>
<div>
<NuxtLayout
name="main"
title="Writing"
description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here."
>
<template #header-upper>
<div class="header-upper-inner">
<NuxtLink
to="/"
class="back-link"
aria-label="Back to Home"
>
<span class="arrow">←</span>
<span>Home</span>
</NuxtLink>
</div>
</template>
<section>
<div class="section__content">
<template v-if="pages?.length">
<NuxtLink
v-for="page in pages"
:to="'/writing/' + page.path"
:key="page.path"
class="article"
>
<h3 class="article__title">{{ page.title }}</h3>
<!-- <p class="article__date">
{{ formatDate(page.date) }}
</p> -->
<p class="article__description">
{{ page.description }}
</p>
</NuxtLink>
</template>
<template v-else>
<div class="empty-state">
<p>No thoughts...head empty</p>
</div>
</template>
</div>
</section>
<article>
<WritingList :pages="pages ?? []" />
</article>
</NuxtLayout>
</div>
</template>
<style scoped>
.header-upper-inner {
display: flex;
align-items: center;
height: 100%;
padding: 0 2rem;
}
section > .section__content {
padding: 0 2rem;
}
section:not(:has(h5)) > .section__content {
padding: 2rem;
}
.article {
border-bottom: 1px solid var(--muted);
padding: 1rem 0;
display: block;
text-decoration: none;
color: inherit;
}
.article:first-of-type {
padding-top: 0;
}
.article:last-of-type {
padding-bottom: 0;
}
.article:last-child {
border-bottom: none;
}
.article:hover > .article__title {
text-decoration: underline;
text-underline-offset: 4px;
font-style: italic;
font-family: var(--font-hover);
}
.article__title {
margin: 0;
font-weight: 400;
}
.article__date {
margin: 0;
font-weight: 400;
font-size: small;
color: var(--muted);
}
.article__description {
margin: 0;
font-size: small;
font-weight: 400;
font-style: italic;
}
.empty-state {
color: var(--muted);
}
</style>
-13
View File
@@ -1,13 +0,0 @@
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
export default defineNuxtPlugin(() => {
gsap.registerPlugin(ScrollTrigger);
return {
provide: {
gsap,
ScrollTrigger,
},
};
});
+44
View File
@@ -0,0 +1,44 @@
export const BODY_FONTS = [
{ id: 'plantin', label: 'Plantin' },
{ id: 'palatino', label: 'Palatino' },
{ id: 'redaction', label: 'Redaction' },
// These are to be used by components or other styles
// { id: 'redaction-10', label: 'Redaction 10' },
// { id: 'redaction-20', label: 'Redaction 20' },
// { id: 'redaction-35', label: 'Redaction 35' },
// { id: 'redaction-50', label: 'Redaction 50' },
// { id: 'redaction-70', label: 'Redaction 70' },
// { id: 'redaction-100', label: 'Redaction 100' },
{ id: 'sohne', label: 'Sohne' },
{ id: 'georgia', label: 'Georgia' },
{ id: 'system', label: 'System Sans' },
{ id: 'space', label: 'Space Mono' },
] as const;
export type BodyFontId = (typeof BODY_FONTS)[number]['id'];
export const DEFAULT_BODY_FONT: BodyFontId = 'plantin';
export const BODY_FONT_STORAGE_KEY = 'body-font';
export function isBodyFontId(value: string | null): value is BodyFontId {
return BODY_FONTS.some((font) => font.id === value);
}
export const REDACTION_STRENGTHS = [0, 10, 20, 35, 50, 70, 100] as const;
export type RedactionStrength = (typeof REDACTION_STRENGTHS)[number];
export function redactionFamily(strength: RedactionStrength): string {
return strength === 0 ? 'Redaction' : `Redaction ${strength}`;
}
export function resolveRedactionStrength(value: unknown): RedactionStrength {
const n = Number(value);
if (REDACTION_STRENGTHS.includes(n as RedactionStrength)) {
return n as RedactionStrength;
}
if (!Number.isFinite(n)) return 35;
return REDACTION_STRENGTHS.reduce((best, grade) =>
Math.abs(grade - n) < Math.abs(best - n) ? grade : best,
);
}
+57
View File
@@ -0,0 +1,57 @@
const WIDE = '(min-width: 1200px)';
const GAP = 8;
export function stackMarginNotes() {
if (!import.meta.client) return;
const notes = Array.from(
document.querySelectorAll<HTMLElement>('.margin-note'),
);
if (!window.matchMedia(WIDE).matches) {
for (const note of notes) note.style.top = '';
return;
}
for (const note of notes) note.style.top = '';
let previousBottom = Number.NEGATIVE_INFINITY;
for (const note of notes) {
const article = note.closest('article');
const mark = note
.closest('.margin-note-root')
?.querySelector<HTMLElement>('.margin-note-mark');
if (!article || !mark) continue;
const articleTop = article.getBoundingClientRect().top;
let top = mark.getBoundingClientRect().top - articleTop;
if (top < previousBottom + GAP) {
top = previousBottom + GAP;
}
note.style.top = `${Math.max(0, top)}px`;
previousBottom = top + note.offsetHeight;
}
}
let listening = false;
export function watchMarginNotes() {
if (!import.meta.client || listening) {
stackMarginNotes();
return;
}
listening = true;
const run = () => stackMarginNotes();
window.addEventListener('resize', run);
document.fonts?.ready.then(run);
new ResizeObserver(run).observe(document.documentElement);
new MutationObserver(run).observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
run();
}
+22
View File
@@ -0,0 +1,22 @@
export const BG_THEMES = [
{ id: 'dusk', label: 'Dusk' },
{ id: 'navy', label: 'Navy' },
{ id: 'midnight', label: 'Midnight' },
{ id: 'indigo', label: 'Indigo' },
{ id: 'storm', label: 'Storm' },
{ id: 'slate', label: 'Slate' },
{ id: 'ink', label: 'Ink' },
{ id: 'pine', label: 'Pine' },
{ id: 'wine', label: 'Wine' },
{ id: 'cocoa', label: 'Cocoa' },
{ id: 'paper', label: 'Paper' },
] as const;
export type BgThemeId = (typeof BG_THEMES)[number]['id'];
export const DEFAULT_BG_THEME: BgThemeId = 'dusk';
export const BG_THEME_STORAGE_KEY = 'bg-theme';
export function isBgThemeId(value: string | null): value is BgThemeId {
return BG_THEMES.some((theme) => theme.id === value);
}
+1
View File
@@ -12,6 +12,7 @@ export default defineContentConfig({
title: z.string(),
date: z.coerce.date(),
description: z.string(),
demo: z.boolean().optional(),
}),
}),
},
@@ -1,6 +1,6 @@
---
title: 'hot take: frontend developers are underpaid.'
date: 2026-05-03
date: 2026-09-22
description: that shit is hard
---
+117
View File
@@ -0,0 +1,117 @@
---
title: Notes in the margin
date: 2026-08-22
description: Every mark this site knows how to make — body type, asides, notices, redaction, quotes, lists, code, and images.
demo: true
---
This page is a working sample, not an essay. Walk it when you change type, color, or a content component. The **description** above is the article lead — bold, then the body.
Ordinary copy sits in the column. You can *emphasize*, **stress**, or [link out](https://jaronpate.com). A thought that does not belong in the sentence can sit beside it. :margin-note[(A margin note. Wide screens put it in the right gutter. Narrow screens drop it under the paragraph.)] Parentheticals you can actually skip.
## Headings
`h2` and `h3` share one italic, muted style. Use them as section marks, not display type.
### A third-level mark
Still the same size and color. Hierarchy is the word, not the weight.
## Margin notes
Two notes in one stretch of text, so you can see them stack.
The first is a mutter. :margin-note[(First note — should sit next to this sentence.)] A little later, a second one arrives. :margin-note[(Second note. On a wide screen these should not collide with the column.)] Between them the column keeps moving.
```md
:margin-note[(like this)]
```
## Notices
A notice interrupts on purpose. It stays in the column.
::notice
**In-column.** Use this when the aside *is* the point for a minute. Navy, hard to miss, still inside the measure.
::
```md
::notice
**In-column.**
::
```
And when it needs to break the column:
::notice{bleed}
**Bleed.** Same idea, edge to edge. Save it for the one time in a piece you actually want to shout.
::
```md
::notice{bleed}
**Bleed.**
::
```
## Redaction
A span can be set in Redaction. `strength` is a grade: `0` is the clean face, `100` is almost gone. Other numbers snap to `0`, `10`, `20`, `35`, `50`, `70`, or `100`. Default is `100`.
```md
:redacted[these words]{strength="50"}
```
Same phrase, every grade:
- 0 — :redacted[the record is still intact]{strength="0"}
- 10 — :redacted[the record is still intact]{strength="10"}
- 20 — :redacted[the record is still intact]{strength="20"}
- 35 — :redacted[the record is still intact]{strength="35"}
- 50 — :redacted[the record is still intact]{strength="50"}
- 70 — :redacted[the record is still intact]{strength="70"}
- 100 — :redacted[the record is still intact]{strength="100"}
Inline, mixed into a sentence: the filing names :redacted[a person we will not repeat]{strength="20"}, then later :redacted[almost nothing remains]{strength="100"}.
**Bold around** :redacted[a redacted span]{strength="50"} should pick Redaction Bold.
## Blockquote
The pointing hand sits in the gutter. Use it for a quote, or a line you want pointed at.
> The best sticking point is the way using the thing *feels*. Everything else is infrastructure.
## Lists
Unordered, for a pile of equals:
- Plantin for reading
- Space Mono for the wordmark, footer, and code
- Redaction only where you ask for it
Ordered, when sequence matters:
1. Write the sentence
2. Park the mutter in a margin note
3. Redact what should stay almost-said
## Code
Inline: the helper is `resolveRedactionStrength`, and the shortcode is `:redacted`.
A block:
```ts
const grade = resolveRedactionStrength(strength);
const family = redactionFamily(grade);
```
## Image
A figure in the column. This is an old Edlink shot, just to see the matte and radius.
![Billing overview](/work/edlink/billing-overview.png)
---
That’s the set. Margin for a mutter. Notice for a point. Redaction when the words should still be there, barely. Everything else is ordinary prose.
+22 -6
View File
@@ -27,10 +27,22 @@ export default defineNuxtConfig({
type: 'd1',
bindingName: 'DB',
},
build: {
markdown: {
highlight: {
theme: {
default: 'github-light',
'light-mode': 'github-light',
'dark-mode': 'github-dark',
},
},
},
},
},
css: ['~/assets/css/main.css'],
app: {
pageTransition: { name: 'page', mode: 'out-in' },
head: {
htmlAttrs: { lang: 'en' },
charset: 'utf-8',
@@ -44,17 +56,21 @@ export default defineNuxtConfig({
},
],
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }],
script: [
{
innerHTML: `(function(){try{var t=localStorage.getItem('bg-theme')||'dusk';var f=localStorage.getItem('body-font')||'plantin';document.documentElement.classList.add('theme-'+t,'font-'+f);}catch(e){document.documentElement.classList.add('theme-dusk','font-plantin');}})();`,
tagPosition: 'head',
},
],
},
},
modules: ['@nuxt/content', '@nuxtjs/color-mode'],
colorMode: {
preference: 'system', // default value of $colorMode.preference
fallback: 'light', // fallback value if not system preference found
globalName: '__NUXT_COLOR_MODE__',
componentName: 'ColorScheme',
preference: 'dark',
fallback: 'dark',
classPrefix: '',
classSuffix: '-mode', // Results in .dark-mode or .light-mode
classSuffix: '-mode',
storageKey: 'color-mode',
},
});