rework to 2000's-ish style site

This commit is contained in:
2026-08-22 11:59:33 -05:00
parent 5f7d8d03df
commit ce7ed0fd37
20 changed files with 1052 additions and 1175 deletions
-3
View File
@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import '~/assets/css/main.css';
import { import {
DEFAULT_DESCRIPTION, DEFAULT_DESCRIPTION,
DEFAULT_IMAGE, DEFAULT_IMAGE,
@@ -41,8 +40,6 @@ useHead({
}, },
], ],
}); });
onMounted(() => {});
</script> </script>
<template> <template>
+98 -133
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 */ /* 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-face {
font-family: 'Plantin MT Pro'; font-family: 'Plantin MT Pro';
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProRg.TTF') src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProRg.TTF')
@@ -46,18 +55,73 @@
@font-face { @font-face {
font-family: 'Plantin MT Pro'; 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'); format('truetype');
font-weight: 300; font-weight: 600;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Plantin MT Pro'; 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'); format('truetype');
font-weight: 300; font-weight: 600;
font-style: italic;
font-display: swap;
}
@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;
}
/* Alegreya */
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Alegreya';
src: url('~/assets/fonts/Alegreya HT/Alegreya-BoldItalic.woff2')
format('woff2');
font-weight: 700;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
@@ -70,13 +134,7 @@
font-style: normal; font-style: normal;
font-display: swap; 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-face {
font-family: 'Redaction'; font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction-Italic.woff2') format('woff2'); src: url('~/assets/fonts/Redaction/Redaction-Italic.woff2') format('woff2');
@@ -85,57 +143,14 @@
font-display: swap; font-display: swap;
} }
/* Redaction 10 */
@font-face { @font-face {
font-family: 'Redaction 10'; font-family: 'Redaction';
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.woff2') src: url('~/assets/fonts/Redaction/Redaction-Bold.woff2') format('woff2');
format('woff2');
font-weight: 400;
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-weight: 700;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Redaction 20 */
@font-face {
font-family: 'Redaction 20';
src: url('~/assets/fonts/Redaction/Redaction_20-Regular.woff2')
format('woff2');
font-weight: 400;
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')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Redaction 35 */
@font-face { @font-face {
font-family: 'Redaction 35'; font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Regular.woff2') src: url('~/assets/fonts/Redaction/Redaction_35-Regular.woff2')
@@ -144,13 +159,7 @@
font-style: normal; font-style: normal;
font-display: swap; 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-face {
font-family: 'Redaction 35'; font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Italic.woff2') src: url('~/assets/fonts/Redaction/Redaction_35-Italic.woff2')
@@ -160,80 +169,36 @@
font-display: swap; font-display: swap;
} }
/* Redaction 50 */
@font-face { @font-face {
font-family: 'Redaction 50'; font-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.woff2') src: url('~/assets/fonts/Redaction/Redaction_35-Bold.woff2') format('woff2');
format('woff2');
font-weight: 400;
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-weight: 700;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
/* Space Mono */
@font-face { @font-face {
font-family: 'Redaction 50'; font-family: 'Space Mono';
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2') src: url('~/assets/fonts/Space Mono/SpaceMono-Regular.ttf')
format('woff2'); 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-weight: 400;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
/* Redaction 70 */
@font-face { @font-face {
font-family: 'Redaction 70'; font-family: 'Space Mono';
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2') src: url('~/assets/fonts/Space Mono/SpaceMono-Bold.ttf') format('truetype');
format('woff2');
font-weight: 400;
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-weight: 700;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Italic.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Redaction 100 */
@font-face {
font-family: 'Redaction 100';
src: url('~/assets/fonts/Redaction/Redaction_100-Regular.woff2')
format('woff2');
font-weight: 400;
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')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/*@import url('https://fonts.googleapis.com/css2?family=Young+Serif&display=swap');*/
+310 -95
View File
@@ -1,163 +1,378 @@
@import './fonts.css'; @import './fonts.css';
:root { :root {
/*--font: 'Baskervville', serif;*/ --font: 'Plantin MT Pro', Georgia, serif;
/*--font: 'EB Garamond', serif;*/ --font-plain: 'SohneBuch', -apple-system, BlinkMacSystemFont, sans-serif;
--font: 'Plantin MT Pro', serif; --font-mono: 'Space Mono', ui-monospace, 'SF Mono', Menlo, monospace;
/*--font: 'Young Serif', serif;*/
/*--font: 'SohneBuch', sans-serif;*/ /* paper + ink from the old site; accent/navy sampled from blend.jpeg */
/*--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;
--bg: #f6f7f9; --bg: #f6f7f9;
--bg-secondary: #e9ebef; --bg-secondary: #e9ebef;
--dot-color: #666; --text: #2e3140;
--muted: #6a6d7a;
--color-white-500: #f6f7f9; --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; --gap: 1rem;
--radius: 4px; --radius: 4px;
} }
::selection { html.font-plantin {
background: var(--text); --font: 'Plantin MT Pro', Georgia, serif;
color: var(--bg); --body-weight: 300;
} }
.dark-mode { html.font-alegreya {
--font: 'Alegreya', Georgia, serif;
--body-weight: 400;
}
html.font-redaction {
--font: 'Redaction', Georgia, serif;
--body-weight: 400;
}
html.font-redaction-35 {
--font: 'Redaction 35', Georgia, serif;
--body-weight: 400;
}
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-iowan {
--font: 'Iowan Old Style', 'Palatino Linotype', Palatino, serif;
--body-weight: 400;
}
html.font-system {
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--body-weight: 400;
}
html.font-courier {
--font: 'Courier New', Courier, ui-monospace, monospace;
--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; --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: #0c0c0c;
--bg-secondary: #1c1c1c; --bg-secondary: #1c1c1c;
--dot-color: #666; --hairline: #2a2a2a;
--muted: #a3a3a3; --code-bg: #221c20;
--img-matte: #1c1c1c;
--color-white-500: #fbfbf7;
} }
.flex { html.theme-pine {
display: flex; --bg: #121c22;
--bg-secondary: #1a262c;
--hairline: #2a3840;
--code-bg: #1c2428;
--img-matte: #1a262c;
} }
.flex-center { html.theme-wine {
display: flex; --bg: #1a1520;
justify-content: center; --bg-secondary: #261c28;
align-items: center; --hairline: #3a2c38;
--code-bg: #2a1e28;
--img-matte: #261c28;
} }
.flex-ff { html.theme-cocoa {
flex: 1; --bg: #1c1814;
} --bg-secondary: #2a241c;
--hairline: #3c3428;
.row { --code-bg: #2c241c;
flex-direction: row; --img-matte: #2a241c;
}
.col {
flex-direction: column;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
::selection {
background: var(--accent);
color: #fff;
}
html, html,
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: var(--bg); background: var(--bg);
overscroll-behavior-y: none; color: var(--text);
overscroll-behavior-x: auto; overflow-x: clip;
} }
body { body {
font-family: var(--font); font-family: var(--font);
color: var(--text);
font-size: 16px; 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 { body::after {
content: ''; content: '';
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999; z-index: 9999;
pointer-events: none; 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"); 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; mix-blend-mode: multiply;
} }
html:not(.theme-paper) body::after,
.dark-mode body::after { .dark-mode body::after {
mix-blend-mode: screen; mix-blend-mode: screen;
opacity: 0.045; opacity: 0.04;
} }
/* quick page transition */ a {
.page-enter-active, color: var(--accent);
.page-leave-active { text-decoration: none;
transition: opacity 0.18s ease; text-underline-offset: 3px;
} }
.page-enter-from, a:hover {
.page-leave-to { text-decoration: underline;
opacity: 0;
} }
.basic-link { article {
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;
position: relative; position: relative;
color: var(--color-white-500); width: var(--col);
text-decoration: none; margin-left: var(--col-left);
transition: all 200ms ease; margin-right: auto;
padding: 0 4px; overflow-wrap: break-word;
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;
} }
.punch-link:hover { article h2,
background-size: 100% 100%; article h3 {
background-image: linear-gradient(#fff, #fff); font-weight: 400;
color: #000; font-style: italic;
color: var(--muted);
font-size: 1rem;
margin: 2.25rem 0 0.75rem;
line-height: 1.3;
} }
.back-link { article > h3:first-child {
display: flex; margin-top: 0;
align-items: center; }
gap: 0.5rem;
font-size: small; article p {
margin: 0 0 1em;
}
article ul,
article ol {
margin: 0 0 1em;
padding-left: 1.2em;
}
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); color: var(--text);
text-decoration: none;
transition: opacity 0.2s ease;
} }
.back-link .arrow { article blockquote p:last-child {
transition: transform 0.2s ease; margin-bottom: 0;
} }
.back-link:hover .arrow { article blockquote::before {
transform: translateX(-4px); 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;
background: var(--code-bg);
padding: 0.12em 0.35em;
border-radius: 3px;
}
pre {
margin: 0 0 1em;
padding: 0.9rem 1rem;
background: var(--bg-secondary);
border-radius: var(--radius);
overflow-x: auto;
line-height: 1.55;
}
pre code {
padding: 0;
background: none;
font-size: 0.8rem;
}
@media (max-width: 640px) {
body {
font-size: 17px;
}
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
<template>
<span class="margin-note"><slot /></span>
</template>
<style scoped>
.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% + 2rem);
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>
+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 };
}
+11 -64
View File
@@ -9,68 +9,15 @@ useHead({
</script> </script>
<template> <template>
<div class="error-page"> <NuxtLayout
<svg width="0" height="0" aria-hidden="true" style="position: absolute"> name="main"
<filter id="grainy" x="0" y="0" width="100%" height="100%"> :title="String(props.error.statusCode ?? 500)"
<feTurbulence :description="props.error.message ?? 'Sorry, something went wrong.'"
type="fractalNoise" >
baseFrequency=".85" <article>
numOctaves="3" <p>
></feTurbulence> <NuxtLink to="/">Back to the homepage.</NuxtLink>
<feColorMatrix type="saturate" values="0"></feColorMatrix> </p>
<feBlend mode="multiply" in="SourceGraphic"></feBlend> </article>
</filter> </NuxtLayout>
</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
>
</p>
</div>
</div>
</div>
</template> </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>
+248 -279
View File
@@ -1,332 +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"> <script setup lang="ts">
defineProps<{ withDefaults(
title?: string; defineProps<{
description?: string; 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 = () => { const onThemeChange = (event: Event) => {
theme.preference = theme.preference === 'light' ? 'dark' : 'light'; const value = (event.target as HTMLSelectElement).value;
const next = themes.find((theme) => theme.id === value);
if (next) setTheme(next.id);
}; };
const { $gsap } = useNuxtApp(); const onFontChange = (event: Event) => {
const pageEl = useTemplateRef('pageEl'); const value = (event.target as HTMLSelectElement).value;
const initialized = ref(false); const next = fonts.find((font) => font.id === value);
if (next) setFont(next.id);
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());
});
</script> </script>
<template> <template>
<div v-show="initialized"> <div class="site">
<svg width="0" height="0" aria-hidden="true" style="position: absolute"> <header class="hero">
<filter id="grainy" x="0" y="0" width="100%" height="100%"> <div class="hero__inner">
<feTurbulence <div class="hero__nav">
type="fractalNoise" <NuxtLink to="/" class="wordmark" aria-label="Home">
baseFrequency=".85" @jaronpate
numOctaves="3" </NuxtLink>
></feTurbulence> <nav class="site-nav">
<feColorMatrix type="saturate" values="0"></feColorMatrix> <NuxtLink to="/writing">Writing</NuxtLink>
<feBlend mode="multiply" in="SourceGraphic"></feBlend> <a href="https://github.com/jaronpate">GitHub</a>
</filter> <label class="theme-picker">
</svg> <span class="visually-hidden">Background</span>
<div class="page-wrapper" ref="pageEl"> <select
<header class="header"> class="theme-select"
<div class="header-upper"> :value="id"
<slot name="header-upper"> @change="onThemeChange"
<div class="flex-center header-upper-nav">
<button
class="theme-toggle"
@click="toggleTheme"
v-if="!theme.unknown"
> >
{{ theme.value === 'dark' ? '🌚' : '🌝' }} <option
</button> v-for="theme in themes"
<div class="flex-ff"></div> :key="theme.id"
<a :value="theme.id"
class="basic-link" >
href="https://github.com/jaronpate" {{ theme.label }}
</option>
</select>
</label>
<label class="theme-picker">
<span class="visually-hidden">Font</span>
<select
class="theme-select"
:value="fontId"
@change="onFontChange"
> >
GitHub <option
</a> v-for="font in fonts"
<NuxtLink to="/writing" class="basic-link"> :key="font.id"
Writing :value="font.id"
</NuxtLink> >
</div> {{ font.label }}
</slot> </option>
</select>
</label>
</nav>
</div> </div>
<div class="header-lower"> <h1 class="hero__title">{{ title }}</h1>
<slot name="header-lower"> <p v-if="description" class="hero__sub">{{ description }}</p>
<div class="mask"> </div>
<h1 class="name"> </header>
{{ title || "Hey, I'm Jaron" }}
</h1>
</div>
<div class="mask">
<p class="title">
{{
description ||
'Software Engineer by trade — builder by nature.'
}}
</p>
</div>
</slot>
</div>
</header>
<main> <div class="page-body">
<slot /> <slot />
<div style="flex: 1"></div>
</main>
<footer class="footer">
<div class="footer-upper flex-center">
<p>
<a class="punch-link" href="mailto:yo@jp.wtf"
>Say hi.</a
>
</p>
<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>
</footer>
</div> </div>
<footer class="site-footer">
<a href="mailto:yo@jp.wtf">yo@jp.wtf</a>
<div class="flex-ff"></div>
<span>Est. 2003</span>
<span>Austin, TX</span>
</footer>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.page-wrapper { .site {
--main-width: 50vw; width: 100%;
width: var(--main-width);
margin: 0 auto;
display: flex;
flex-direction: column;
/*gap: 1.5rem;*/
min-height: 100vh; min-height: 100vh;
position: relative; display: flex;
flex-direction: column;
} }
main { .page-body {
flex: 1; 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; 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'] { .wordmark,
position: fixed; .site-nav a,
.theme-select {
color: var(--hero-text);
} }
.mask { .wordmark {
overflow: hidden; display: flex;
} align-items: center;
gap: 0.7rem;
.name { text-decoration: none;
font-size: 3rem; font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 400; font-weight: 400;
line-height: 1.2; line-height: 1;
margin: 0;
} }
.title { .wordmark:hover,
font-weight: 300; .wordmark.router-link-active {
margin: 0; color: var(--hero-text);
font-style: italic; text-decoration: none;
color: var(--color-white-500); opacity: 0.75;
} }
.header { .site-nav {
display: flex; display: flex;
flex-direction: column; align-items: center;
}
.footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.header-upper {
height: 8vh;
min-height: 3.5rem;
}
.header-upper-nav {
padding: 0 2rem;
height: 100%;
font-size: small;
gap: 1rem; gap: 1rem;
margin-left: auto;
} }
.header-lower { .site-nav a:hover,
display: flex; .site-nav a.router-link-active {
flex-direction: column; color: var(--hero-text);
align-items: flex-start; opacity: 0.7;
justify-content: center;
gap: 16px;
padding: 3rem 2.5rem;
color: var(--color-white-500);
border-radius: 16px;
overflow: hidden;
position: relative;
isolation: isolate;
} }
.header-lower::before { .theme-picker {
content: ''; display: inline-flex;
position: absolute; align-items: center;
inset: 0; }
background: url('~/assets/images/blend.jpeg');
background-size: cover; .theme-select {
background-position: left top; 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; background-repeat: no-repeat;
filter: url(#grainy); background-position: right 0.55em center;
border-radius: 16px; background-size: 0.5em;
z-index: -1; border: 1px solid rgba(251, 251, 247, 0.35);
} border-radius: 4px;
.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: 0 2rem;
height: 8vh;
min-height: 3.5rem;
font-size: small;
gap: 1rem;
}
.theme-toggle {
background: transparent;
border: none;
cursor: pointer; cursor: pointer;
font-size: large; font-family: var(--font-plain);
font-weight: bold; font-size: 0.8rem;
color: var(--text); line-height: 1;
transition: opacity 0.2s ease; 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; padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
} }
.theme-toggle:hover { .hero__title {
opacity: 0.5;
}
.footer p {
margin: 0; margin: 0;
font-size: 2.75rem;
font-weight: 400;
line-height: 1.15;
} }
@media (max-width: 1024px) { .hero__sub {
.page-wrapper { margin: 0.6rem 0 0;
--main-width: 80vw; font-weight: var(--body-weight);
} font-style: italic;
font-size: 1.05rem;
color: rgba(251, 251, 247, 0.88);
} }
@media (max-width: 768px) { .site-footer {
.header-upper-nav { --flare-length: 1.125rem;
padding: 0 1.5rem; --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;
} }
.name { .hero__nav {
flex-wrap: wrap;
margin-bottom: 1.75rem;
}
.hero__title {
font-size: 2rem; font-size: 2rem;
} }
.title {
font-size: 1rem;
}
.page-wrapper {
--main-width: 100vw;
padding: 0 1rem;
}
.header-lower,
.footer-upper {
padding: 1.5rem 1.5rem;
border-radius: 12px;
}
.footer-lower {
padding: 1.5rem 1.5rem;
}
.header-lower::before,
.footer-upper::before {
border-radius: 12px;
}
} }
</style> </style>
+66 -437
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"> <script setup lang="ts">
import { ref } from 'vue';
import { import {
DEFAULT_DESCRIPTION, DEFAULT_DESCRIPTION,
DEFAULT_IMAGE, DEFAULT_IMAGE,
@@ -36,14 +30,6 @@ useHead({
link: [{ rel: 'canonical', href: SITE_URL }], link: [{ rel: 'canonical', href: SITE_URL }],
}); });
const titles = [
"Hey, I'm Jaron.",
'Nice to meet you.',
'Welcome.',
'Willkommen.',
'Bienvenue.',
];
const selectedWorks = [ const selectedWorks = [
{ {
title: 'Edlink', title: 'Edlink',
@@ -186,312 +172,116 @@ 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({ definePageMeta({
layout: false, 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> </script>
<template> <template>
<div ref="pageRoot"> <NuxtLayout name="main">
<NuxtLayout name="main"> <article>
<section class="rows"> <h3>Selected Work</h3>
<h5>Selected Work</h5> <template v-for="work in selectedWorks" :key="work.title">
<div class="section__content"> <p>
<a <a :href="work.link" target="_blank" rel="noopener">{{
v-for="work in selectedWorks" work.title
:key="work.title" }}</a>
class="work" — {{ work.description }}
:href="work.link" </p>
target="_blank" <div class="work__images">
<figure
v-for="image in work.images"
:key="image.src"
class="work__image"
> >
<h3 class="work__title">{{ work.title }}</h3> <img :src="image.src" :alt="image.alt" />
<p class="work__description"> </figure>
{{ work.description }}
</p>
<div class="work__images">
<figure
v-for="image in work.images"
:key="image.src"
class="work__image"
>
<img :src="image.src" :alt="image.alt" />
</figure>
</div>
</a>
</div> </div>
</section> </template>
<section class="rows">
<h5>Projects</h5> <h3>Projects</h3>
<div class="section__content"> <ul>
<a <li v-for="project in projects" :key="project.href">
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">
{{ project.description }}
</p>
</a>
</div>
</section>
<section class="about">
<h5>About</h5>
<div class="section__content">
<p> <p>
I left school, my job, and moved to Austin in 2022 to write <a :href="project.href" target="_blank" rel="noopener">{{
code full time. That was definitely one of the best and project.title
most life changing decisions I've ever made. I've learned }}</a>
a lot since then and <em>still</em> have much more to learn. — {{ project.description }}
</p> </p>
<p> </li>
These days I build things because I find joy in making something </ul>
someone else really enjoys using. I get excited about reaching a
level of polish that users don't even notice. <h3>About</h3>
</p> <p>
<p> I left school, my job, and moved to Austin in 2022 to write code
In a time where code is cheap I pride myself in taste and execution. full time. That was definitely one of the best and most life
The best sticking point — in my limited opinion so far — is the changing decisions I've ever made. I've learned a lot since then
way using your product <em>feels</em>. So I want everything I make to and <em>still</em> have much more to learn.
simply <em>feel</em> the best to use. </p>
</p> <p>
<p> These days I build things because I find joy in making something
If you want to build something cool or connect someone else really enjoys using. I get excited about reaching a
<a class="basic-link" href="mailto:yo@jp.wtf">hit me up</a> level of polish that users don't even notice.
— we'll grab a coffee or beer if you're in Austin! I </p>
still dont know enough people to form a band yet ;) <p>
</p> In a time where code is cheap I pride myself in taste and
</div> execution. The best sticking point — in my limited opinion so far
</section> — is the way using your product <em>feels</em>. So I want
<!-- <section class="photos"> everything I make to simply <em>feel</em> the best to use.
<h5>Photos</h5> </p>
<div class="section__content"> <p>
<div class="photo-grid"> If you want to build something cool or connect
<div <a href="mailto:yo@jp.wtf">hit me up</a> — we'll grab a coffee or
v-for="photo in photos" beer if you're in Austin! I still dont know enough people to form
:key="photo.src" a band yet ;)
class="photo-grid__item" </p>
> </article>
<div class="photo__image"> </NuxtLayout>
<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> -->
</NuxtLayout>
</div>
</template> </template>
<style scoped> <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 { .work__images {
/*display: flex; --dot-color: #666;
flex-wrap: no-wrap;
align-items: center;
justify-content: center;*/
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
grid-auto-columns: max-content; grid-auto-columns: max-content;
gap: var(--gap); gap: var(--gap);
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
margin-top: var(--gap); margin: 1.25rem 0 1.75rem;
padding-bottom: 0.35rem;
} }
.work__image { .work__image {
--dot-size: 6%; --dot-size: 6%;
margin: 0; margin: 0;
/*padding: calc(var(--gap) * 2) var(--gap) var(--gap);*/
padding: var(--gap); padding: var(--gap);
flex-shrink: 0; flex-shrink: 0;
border-radius: var(--radius); border-radius: var(--radius);
background-color: var(--bg-secondary);
background-image: radial-gradient( background-image: radial-gradient(
circle at 4px 4px, circle at 4px 4px,
var(--dot-color) var(--dot-size), var(--dot-color) var(--dot-size),
transparent calc(var(--dot-size) + 1%) transparent calc(var(--dot-size) + 1%)
); );
background-position: 4px 4px; background-position: 4px 4px;
background-size: 12px 12px; background-size: 12px 12px;
background-repeat: repeat; background-repeat: repeat;
background-color: var(--bg-secondary);
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column;
gap: var(--gap);
} }
.work__image img { .work__image img {
border-radius: var(--radius); max-width: none;
object-fit: cover; width: auto;
height: auto;
max-height: 350px; max-height: 350px;
object-fit: cover;
border-radius: var(--radius);
background: none;
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
@@ -499,165 +289,4 @@ section:last-of-type {
max-height: 300px; 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) {
section > .section__content {
padding: 0 var(--gap);
}
.photo-grid {
grid-template-columns: 1fr;
}
}
</style> </style>
+30 -45
View File
@@ -12,10 +12,14 @@ const articleUrl = `${SITE_URL}${route.path}`;
const webpageId = `${articleUrl}#webpage`; const webpageId = `${articleUrl}#webpage`;
const articleId = `${articleUrl}#article`; const articleId = `${articleUrl}#article`;
const blogId = `${SITE_URL}/writing#blog`; const blogId = `${SITE_URL}/writing#blog`;
const contentPath =
'/' +
(Array.isArray(route.params.slug)
? route.params.slug.join('/')
: String(route.params.slug ?? ''));
const { data: page } = await useAsyncData(route.path, () => { const { data: page } = await useAsyncData(route.path, () => {
return queryCollection('writing') return queryCollection('writing').path(contentPath).first();
.path(route.path.replace('/writing/', ''))
.first();
}); });
if (!page.value) { if (!page.value) {
@@ -134,52 +138,33 @@ useHead({
], ],
}); });
const formatDate = (date: Date | string) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
definePageMeta({ definePageMeta({
layout: false, layout: false,
}); });
</script> </script>
<template> <template>
<div> <NuxtLayout
<NuxtLayout name="main"
name="main" :title="page.title"
:title="page?.title" :description="formatDate(page.date)"
:description="page?.description" >
> <article v-if="page">
<template #header-upper> <div class="article-body">
<div class="header-upper-inner"> <p v-if="page.description">
<NuxtLink <strong>{{ page.description }}</strong>
to="/writing" </p>
class="back-link" <ContentRenderer :value="page" />
aria-label="Back to writing" </div>
> </article>
<span class="arrow">←</span> </NuxtLayout>
<span>Back</span>
</NuxtLink>
</div>
</template>
<ContentRenderer
class="writing__content"
v-if="page"
:value="page"
/>
</NuxtLayout>
</div>
</template> </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>
+61 -100
View File
@@ -7,11 +7,9 @@ import {
websiteId, websiteId,
} from '~/utils/site'; } from '~/utils/site';
// const { data: pages } = await useAsyncData('writing-pages', () => const { data: pages } = await useAsyncData('writing-pages', () =>
// queryCollection('writing').order('date', 'DESC').all(), queryCollection('writing').order('date', 'DESC').all(),
// ); );
const pages = ref<any[]>([]);
const writingUrl = `${SITE_URL}/writing`; const writingUrl = `${SITE_URL}/writing`;
const collectionId = `${writingUrl}#collection`; const collectionId = `${writingUrl}#collection`;
@@ -74,7 +72,7 @@ useHead({
name: SITE_NAME, name: SITE_NAME,
}, },
blogPost: (pages.value ?? []).map((page) => ({ blogPost: (pages.value ?? []).map((page) => ({
'@id': `${writingUrl}/${page.path}#article`, '@id': `${writingUrl}${page.path}#article`,
})), })),
}, },
{ {
@@ -85,7 +83,7 @@ useHead({
(page, index) => ({ (page, index) => ({
'@type': 'ListItem', '@type': 'ListItem',
position: index + 1, position: index + 1,
url: `${writingUrl}/${page.path}`, url: `${writingUrl}${page.path}`,
name: page.title, name: page.title,
}), }),
), ),
@@ -114,10 +112,13 @@ useHead({
], ],
}); });
const formatDate = (dateString: string) => { const formatDate = (dateString: string | Date) => {
const date = new Date(dateString); return new Date(dateString).toLocaleDateString('en-US', {
// ex. May 5, 2026 month: 'short',
return `${date.toLocaleDateString('en-US', { month: 'short' })} ${date.getDate()}, ${date.getFullYear()}`; day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
}; };
definePageMeta({ definePageMeta({
@@ -126,116 +127,76 @@ definePageMeta({
</script> </script>
<template> <template>
<div> <NuxtLayout
<NuxtLayout name="main"
name="main" title="Writing"
title="Writing" description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here."
description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here." >
> <article>
<template #header-upper> <template v-if="pages?.length">
<div class="header-upper-inner"> <NuxtLink
<NuxtLink v-for="page in pages"
to="/" :key="page.path"
class="back-link" :to="'/writing' + page.path"
aria-label="Back to Home" class="listing"
> >
<span class="arrow">←</span> <span class="listing__title">{{ page.title }}</span>
<span>Home</span> <time :datetime="new Date(page.date).toISOString()">{{
</NuxtLink> formatDate(page.date)
</div> }}</time>
</NuxtLink>
</template> </template>
<section> <p v-else class="empty-state">No thoughts...head empty</p>
<div class="section__content"> </article>
<template v-if="pages?.length"> </NuxtLayout>
<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>
</NuxtLayout>
</div>
</template> </template>
<style scoped> <style scoped>
.header-upper-inner { .listing {
display: flex; display: flex;
align-items: center; align-items: baseline;
height: 100%; gap: 1rem;
padding: 0 2rem; margin: 0 0 0.65rem;
}
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; text-decoration: none;
color: inherit; color: inherit;
} }
.article:first-of-type { .listing:hover {
padding-top: 0; text-decoration: none;
} }
.article:last-of-type { .listing:hover .listing__title {
padding-bottom: 0;
}
.article:last-child {
border-bottom: none;
}
.article:hover > .article__title {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 4px; text-underline-offset: 3px;
font-style: italic; font-style: italic;
font-family: var(--font-hover);
} }
.article__title { .listing__title {
margin: 0; color: var(--text);
font-weight: 400;
} }
.article__date { .listing time {
margin: 0; margin-left: auto;
font-weight: 400; font-family: var(--font-plain);
font-size: small; font-size: 0.75rem;
font-style: italic;
color: var(--muted); color: var(--muted);
} white-space: nowrap;
.article__description {
margin: 0;
font-size: small;
font-weight: 400;
font-style: italic;
} }
.empty-state { .empty-state {
color: var(--muted); color: var(--muted);
font-style: italic;
}
@media (max-width: 640px) {
.listing {
flex-direction: column;
gap: 0.15rem;
}
.listing time {
margin-left: 0;
}
} }
</style> </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,
},
};
});
+22
View File
@@ -0,0 +1,22 @@
export const BODY_FONTS = [
{ id: 'plantin', label: 'Plantin' },
{ id: 'alegreya', label: 'Alegreya' },
{ id: 'redaction', label: 'Redaction' },
{ id: 'redaction-35', label: 'Redaction 35' },
{ id: 'sohne', label: 'Sohne' },
{ id: 'space', label: 'Space Mono' },
{ id: 'georgia', label: 'Georgia' },
{ id: 'palatino', label: 'Palatino' },
{ id: 'iowan', label: 'Iowan' },
{ id: 'system', label: 'System Sans' },
{ id: 'courier', label: 'Courier' },
] 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);
}
+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);
}
+23
View File
@@ -0,0 +1,23 @@
---
title: Notes in the margin
date: 2026-08-22
description: A quick look at the two asides this site knows how to make.
---
Most of what I write should just be the thing itself. Sometimes a thought doesn't belong in the sentence, though, it belongs next to it. :margin-note[(like this — a margin note sits beside the column on a wide screen, and drops under the paragraph on a small one)] That's what margin notes are for. Parentheticals you can actually skip.
Notices are the opposite. They interrupt on purpose.
::notice
**A note you shouldn't skip.** Use a notice when the aside *is* the point for a minute. It stays in the column, navy, hard to miss.
::
And if it needs to break the column entirely:
::notice{bleed}
**Full-bleed.** Same idea, but it runs edge to edge. Save it for the one time in a piece you actually want to shout.
::
That's the whole vocabulary. Margin for a mutter. Band for a point. Everything else is just the writing.
> A blockquote gets the little pointing hand in the left margin. Use it when you're quoting someone, or when you want a line to feel pointed at.
+11 -6
View File
@@ -29,8 +29,9 @@ export default defineNuxtConfig({
}, },
}, },
css: ['~/assets/css/main.css'],
app: { app: {
pageTransition: { name: 'page', mode: 'out-in' },
head: { head: {
htmlAttrs: { lang: 'en' }, htmlAttrs: { lang: 'en' },
charset: 'utf-8', charset: 'utf-8',
@@ -44,17 +45,21 @@ export default defineNuxtConfig({
}, },
], ],
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }], 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'], modules: ['@nuxt/content', '@nuxtjs/color-mode'],
colorMode: { colorMode: {
preference: 'system', // default value of $colorMode.preference preference: 'dark',
fallback: 'light', // fallback value if not system preference found fallback: 'dark',
globalName: '__NUXT_COLOR_MODE__',
componentName: 'ColorScheme',
classPrefix: '', classPrefix: '',
classSuffix: '-mode', // Results in .dark-mode or .light-mode classSuffix: '-mode',
storageKey: 'color-mode', storageKey: 'color-mode',
}, },
}); });