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">
import '~/assets/css/main.css';
import {
DEFAULT_DESCRIPTION,
DEFAULT_IMAGE,
@@ -41,8 +40,6 @@ useHead({
},
],
});
onMounted(() => {});
</script>
<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 */
@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,18 +55,73 @@
@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;
}
@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-display: swap;
}
@@ -70,13 +134,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,57 +143,14 @@
font-display: swap;
}
/* Redaction 10 */
@font-face {
font-family: 'Redaction 10';
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.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-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-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-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Regular.woff2')
@@ -144,13 +159,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,80 +169,36 @@
font-display: swap;
}
/* Redaction 50 */
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.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-family: 'Redaction 35';
src: url('~/assets/fonts/Redaction/Redaction_35-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Space Mono */
@font-face {
font-family: 'Redaction 50';
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2')
format('woff2');
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;
}
/* Redaction 70 */
@font-face {
font-family: 'Redaction 70';
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2')
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-family: 'Space Mono';
src: url('~/assets/fonts/Space Mono/SpaceMono-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
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';
: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-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;
--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);
overscroll-behavior-y: none;
overscroll-behavior-x: auto;
color: var(--text);
overflow-x: clip;
}
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 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;
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>
<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
>
</p>
</div>
</div>
</div>
<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>
</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>
+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">
defineProps<{
title?: string;
description?: string;
}>();
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"
<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"
>
{{ theme.value === 'dark' ? '🌚' : '🌝' }}
</button>
<div class="flex-ff"></div>
<a
class="basic-link"
href="https://github.com/jaronpate"
<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"
>
GitHub
</a>
<NuxtLink to="/writing" class="basic-link">
Writing
</NuxtLink>
</div>
</slot>
<option
v-for="font in fonts"
:key="font.id"
:value="font.id"
>
{{ font.label }}
</option>
</select>
</label>
</nav>
</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>
</div>
</header>
<h1 class="hero__title">{{ title }}</h1>
<p v-if="description" class="hero__sub">{{ description }}</p>
</div>
</header>
<main>
<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 class="page-body">
<slot />
</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>
</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;
min-height: 3.5rem;
}
.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: 0 2rem;
height: 8vh;
min-height: 3.5rem;
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) {
.header-upper-nav {
padding: 0 1.5rem;
.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;
}
.name {
.hero__nav {
flex-wrap: wrap;
margin-bottom: 1.75rem;
}
.hero__title {
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>
+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">
import { ref } from 'vue';
import {
DEFAULT_DESCRIPTION,
DEFAULT_IMAGE,
@@ -36,14 +30,6 @@ useHead({
link: [{ rel: 'canonical', href: SITE_URL }],
});
const titles = [
"Hey, I'm Jaron.",
'Nice to meet you.',
'Welcome.',
'Willkommen.',
'Bienvenue.',
];
const selectedWorks = [
{
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({
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"
<NuxtLayout name="main">
<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">
<figure
v-for="image in work.images"
:key="image.src"
class="work__image"
>
<h3 class="work__title">{{ work.title }}</h3>
<p class="work__description">
{{ 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>
<img :src="image.src" :alt="image.alt" />
</figure>
</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">
{{ project.description }}
</p>
</a>
</div>
</section>
<section class="about">
<h5>About</h5>
<div class="section__content">
</template>
<h3>Projects</h3>
<ul>
<li v-for="project in projects" :key="project.href">
<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.
<a :href="project.href" target="_blank" rel="noopener">{{
project.title
}}</a>
— {{ project.description }}
</p>
<p>
These days I build things because I find joy in making something
someone else really enjoys using. I get excited about reaching a
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.
</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 ;)
</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> -->
</NuxtLayout>
</div>
</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.
</p>
<p>
These days I build things because I find joy in making something
someone else really enjoys using. I get excited about reaching a
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.
</p>
<p>
If you want to build something cool or connect
<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>
</article>
</NuxtLayout>
</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,165 +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) {
section > .section__content {
padding: 0 var(--gap);
}
.photo-grid {
grid-template-columns: 1fr;
}
}
</style>
+30 -45
View File
@@ -12,10 +12,14 @@ const articleUrl = `${SITE_URL}${route.path}`;
const webpageId = `${articleUrl}#webpage`;
const articleId = `${articleUrl}#article`;
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, () => {
return queryCollection('writing')
.path(route.path.replace('/writing/', ''))
.first();
return queryCollection('writing').path(contentPath).first();
});
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({
layout: false,
});
</script>
<template>
<div>
<NuxtLayout
name="main"
:title="page?.title"
:description="page?.description"
>
<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>
</div>
</template>
<ContentRenderer
class="writing__content"
v-if="page"
:value="page"
/>
</NuxtLayout>
</div>
<NuxtLayout
name="main"
:title="page.title"
:description="formatDate(page.date)"
>
<article v-if="page">
<div class="article-body">
<p v-if="page.description">
<strong>{{ page.description }}</strong>
</p>
<ContentRenderer :value="page" />
</div>
</article>
</NuxtLayout>
</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,
} from '~/utils/site';
// const { data: pages } = await useAsyncData('writing-pages', () =>
// queryCollection('writing').order('date', 'DESC').all(),
// );
const pages = ref<any[]>([]);
const { data: pages } = await useAsyncData('writing-pages', () =>
queryCollection('writing').order('date', 'DESC').all(),
);
const writingUrl = `${SITE_URL}/writing`;
const collectionId = `${writingUrl}#collection`;
@@ -74,7 +72,7 @@ useHead({
name: SITE_NAME,
},
blogPost: (pages.value ?? []).map((page) => ({
'@id': `${writingUrl}/${page.path}#article`,
'@id': `${writingUrl}${page.path}#article`,
})),
},
{
@@ -85,7 +83,7 @@ useHead({
(page, index) => ({
'@type': 'ListItem',
position: index + 1,
url: `${writingUrl}/${page.path}`,
url: `${writingUrl}${page.path}`,
name: page.title,
}),
),
@@ -114,10 +112,13 @@ 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()}`;
const formatDate = (dateString: string | Date) => {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
});
};
definePageMeta({
@@ -126,116 +127,76 @@ definePageMeta({
</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>
<NuxtLayout
name="main"
title="Writing"
description="Thoughts I've had here and there. Maybe you'll find something helpful or interesting here."
>
<article>
<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>
<time :datetime="new Date(page.date).toISOString()">{{
formatDate(page.date)
}}</time>
</NuxtLink>
</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>
</NuxtLayout>
</div>
<p v-else class="empty-state">No thoughts...head empty</p>
</article>
</NuxtLayout>
</template>
<style scoped>
.header-upper-inner {
.listing {
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;
align-items: baseline;
gap: 1rem;
margin: 0 0 0.65rem;
text-decoration: none;
color: inherit;
}
.article:first-of-type {
padding-top: 0;
.listing:hover {
text-decoration: none;
}
.article:last-of-type {
padding-bottom: 0;
}
.article:last-child {
border-bottom: none;
}
.article:hover > .article__title {
.listing:hover .listing__title {
text-decoration: underline;
text-underline-offset: 4px;
text-underline-offset: 3px;
font-style: italic;
font-family: var(--font-hover);
}
.article__title {
margin: 0;
font-weight: 400;
.listing__title {
color: var(--text);
}
.article__date {
margin: 0;
font-weight: 400;
font-size: small;
.listing time {
margin-left: auto;
font-family: var(--font-plain);
font-size: 0.75rem;
font-style: italic;
color: var(--muted);
}
.article__description {
margin: 0;
font-size: small;
font-weight: 400;
font-style: italic;
white-space: nowrap;
}
.empty-state {
color: var(--muted);
font-style: italic;
}
@media (max-width: 640px) {
.listing {
flex-direction: column;
gap: 0.15rem;
}
.listing time {
margin-left: 0;
}
}
</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);
}