mirror of
https://github.com/jaronpate/folio.git
synced 2026-10-19 01:07:39 +00:00
@@ -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>
|
||||||
|
|||||||
+132
-71
@@ -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,23 +55,44 @@
|
|||||||
|
|
||||||
@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-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction */
|
@font-face {
|
||||||
|
font-family: 'Plantin MT Pro';
|
||||||
|
src: url('~/assets/fonts/Plantin MT Pro/PlantinMTProCnBd.TTF')
|
||||||
|
format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: condensed;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sohne Buch */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'SohneBuch';
|
||||||
|
src:
|
||||||
|
url('~/assets/fonts/Sohne/Sohne-Buch.woff') format('woff'),
|
||||||
|
url('~/assets/fonts/Sohne/Sohne-Buch.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Redaction — 7 grades × Regular / Italic / Bold */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction';
|
font-family: 'Redaction';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction-Regular.woff2') format('woff2');
|
src: url('~/assets/fonts/Redaction/Redaction-Regular.woff2') format('woff2');
|
||||||
@@ -70,13 +100,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,7 +109,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 10 */
|
@font-face {
|
||||||
|
font-family: 'Redaction';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 10';
|
font-family: 'Redaction 10';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_10-Regular.woff2')
|
||||||
@@ -94,13 +125,7 @@
|
|||||||
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-Bold.woff2') format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 10';
|
font-family: 'Redaction 10';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_10-Italic.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_10-Italic.woff2')
|
||||||
@@ -110,7 +135,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 20 */
|
@font-face {
|
||||||
|
font-family: 'Redaction 10';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_10-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 20';
|
font-family: 'Redaction 20';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_20-Regular.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_20-Regular.woff2')
|
||||||
@@ -119,13 +151,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
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-face {
|
||||||
font-family: 'Redaction 20';
|
font-family: 'Redaction 20';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_20-Italic.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_20-Italic.woff2')
|
||||||
@@ -135,7 +161,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 35 */
|
@font-face {
|
||||||
|
font-family: 'Redaction 20';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_20-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-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 +177,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,7 +187,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 50 */
|
@font-face {
|
||||||
|
font-family: 'Redaction 35';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_35-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 50';
|
font-family: 'Redaction 50';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_50-Regular.woff2')
|
||||||
@@ -169,13 +203,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
@font-face {
|
|
||||||
font-family: 'Redaction 50';
|
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_50-Bold.woff2') format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 50';
|
font-family: 'Redaction 50';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_50-Italic.woff2')
|
||||||
@@ -185,7 +213,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 70 */
|
@font-face {
|
||||||
|
font-family: 'Redaction 50';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_50-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 70';
|
font-family: 'Redaction 70';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_70-Regular.woff2')
|
||||||
@@ -194,13 +229,7 @@
|
|||||||
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-Bold.woff2') format('woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 70';
|
font-family: 'Redaction 70';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_70-Italic.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_70-Italic.woff2')
|
||||||
@@ -210,7 +239,14 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Redaction 100 */
|
@font-face {
|
||||||
|
font-family: 'Redaction 70';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_70-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Redaction 100';
|
font-family: 'Redaction 100';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_100-Regular.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_100-Regular.woff2')
|
||||||
@@ -219,14 +255,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
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-face {
|
||||||
font-family: 'Redaction 100';
|
font-family: 'Redaction 100';
|
||||||
src: url('~/assets/fonts/Redaction/Redaction_100-Italic.woff2')
|
src: url('~/assets/fonts/Redaction/Redaction_100-Italic.woff2')
|
||||||
@@ -236,4 +265,36 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@import url('https://fonts.googleapis.com/css2?family=Young+Serif&display=swap');*/
|
@font-face {
|
||||||
|
font-family: 'Redaction 100';
|
||||||
|
src: url('~/assets/fonts/Redaction/Redaction_100-Bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Space Mono */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
src: url('~/assets/fonts/Space Mono/SpaceMono-Regular.ttf')
|
||||||
|
format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
src: url('~/assets/fonts/Space Mono/SpaceMono-Italic.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
src: url('~/assets/fonts/Space Mono/SpaceMono-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|||||||
+341
-94
@@ -1,163 +1,410 @@
|
|||||||
@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-sohne {
|
||||||
|
--font: 'SohneBuch', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-space {
|
||||||
|
--font: 'Space Mono', ui-monospace, Menlo, monospace;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-georgia {
|
||||||
|
--font: Georgia, 'Times New Roman', Times, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-palatino {
|
||||||
|
--font: Palatino, 'Palatino Linotype', 'Book Antiqua', serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction {
|
||||||
|
--font: 'Redaction', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-10 {
|
||||||
|
--font: 'Redaction 10', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-20 {
|
||||||
|
--font: 'Redaction 20', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-35 {
|
||||||
|
--font: 'Redaction 35', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-50 {
|
||||||
|
--font: 'Redaction 50', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-70 {
|
||||||
|
--font: 'Redaction 70', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-redaction-100 {
|
||||||
|
--font: 'Redaction 100', Georgia, serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.font-system {
|
||||||
|
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--body-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode,
|
||||||
|
html.theme-navy,
|
||||||
|
html.theme-dusk,
|
||||||
|
html.theme-midnight,
|
||||||
|
html.theme-indigo,
|
||||||
|
html.theme-storm,
|
||||||
|
html.theme-slate,
|
||||||
|
html.theme-ink,
|
||||||
|
html.theme-pine,
|
||||||
|
html.theme-wine,
|
||||||
|
html.theme-cocoa {
|
||||||
--text: #fbfbf7;
|
--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);
|
||||||
|
color: var(--text);
|
||||||
|
overflow-x: clip;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
overscroll-behavior-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ul {
|
||||||
|
list-style-type: '– ';
|
||||||
|
}
|
||||||
|
|
||||||
|
article li {
|
||||||
|
margin: 0 0 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
article li p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article hr {
|
||||||
|
margin: 2.5rem 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--hairline);
|
||||||
|
}
|
||||||
|
|
||||||
|
article blockquote {
|
||||||
|
--hand-w: 2.35rem;
|
||||||
|
--hand-h: calc(var(--hand-w) * 44 / 100);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.15rem 0 0.15rem 3.15rem;
|
||||||
|
border: 0;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.08em;
|
||||||
|
line-height: 1.55;
|
||||||
color: var(--text);
|
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;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 0.12em 0.35em;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light-mode .shiki span {
|
||||||
|
color: var(--shiki-light-mode, var(--shiki-default));
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark-mode .shiki span {
|
||||||
|
color: var(--shiki-dark-mode, var(--text));
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
body {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
pages: Array<{
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
date: string | Date;
|
||||||
|
}>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | Date) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="pages.length">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="page in pages"
|
||||||
|
:key="page.path"
|
||||||
|
:to="'/writing' + page.path"
|
||||||
|
class="listing"
|
||||||
|
>
|
||||||
|
<span class="listing__title">{{ page.title }}</span>
|
||||||
|
<span class="listing__rule" aria-hidden="true"></span>
|
||||||
|
<time :datetime="new Date(page.date).toISOString()">{{
|
||||||
|
formatDate(page.date)
|
||||||
|
}}</time>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
<p v-else class="empty-state">No thoughts...head empty</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.listing {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0 0 0.65rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing:hover .listing__title {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing__title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing__rule {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--hairline);
|
||||||
|
transform: translateY(-0.35em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: var(--font-plain);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: var(--muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.listing {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing__rule {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watchMarginNotes } from '~/utils/stackMarginNotes';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watchMarginNotes();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="margin-note-root">
|
||||||
|
<span class="margin-note-mark" aria-hidden="true"></span>
|
||||||
|
<span class="margin-note"><slot /></span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.margin-note-root {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-note-mark {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-note {
|
||||||
|
display: block;
|
||||||
|
margin: 0.4rem 0 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.margin-note {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(100% + var(--note-gap));
|
||||||
|
width: var(--note-width);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
redactionFamily,
|
||||||
|
resolveRedactionStrength,
|
||||||
|
} from '~/utils/fonts';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
strength?: number | string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
strength: 100,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const grade = computed(() => resolveRedactionStrength(props.strength));
|
||||||
|
const family = computed(
|
||||||
|
() => `'${redactionFamily(grade.value)}', Georgia, serif`,
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="redacted"
|
||||||
|
:data-strength="grade"
|
||||||
|
:style="{ fontFamily: family }"
|
||||||
|
><slot /></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.redacted {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(strong, b) .redacted {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
BODY_FONTS,
|
||||||
|
BODY_FONT_STORAGE_KEY,
|
||||||
|
DEFAULT_BODY_FONT,
|
||||||
|
isBodyFontId,
|
||||||
|
type BodyFontId,
|
||||||
|
} from '~/utils/fonts';
|
||||||
|
|
||||||
|
export function useBodyFont() {
|
||||||
|
const id = useState<BodyFontId>('body-font', () => DEFAULT_BODY_FONT);
|
||||||
|
|
||||||
|
const applyClass = (next: BodyFontId) => {
|
||||||
|
if (!import.meta.client) return;
|
||||||
|
for (const font of BODY_FONTS) {
|
||||||
|
document.documentElement.classList.remove(`font-${font.id}`);
|
||||||
|
}
|
||||||
|
document.documentElement.classList.add(`font-${next}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFont = (next: BodyFontId) => {
|
||||||
|
id.value = next;
|
||||||
|
applyClass(next);
|
||||||
|
if (import.meta.client) {
|
||||||
|
localStorage.setItem(BODY_FONT_STORAGE_KEY, next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const saved = localStorage.getItem(BODY_FONT_STORAGE_KEY);
|
||||||
|
const next = isBodyFontId(saved) ? saved : id.value;
|
||||||
|
setFont(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id, setFont, fonts: BODY_FONTS };
|
||||||
|
}
|
||||||
+9
-62
@@ -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"
|
|
||||||
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
|
|
||||||
>
|
>
|
||||||
|
<article>
|
||||||
|
<p>
|
||||||
|
<NuxtLink to="/">Back to the homepage.</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
</NuxtLayout>
|
||||||
</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>
|
|
||||||
|
|||||||
+241
-253
@@ -1,313 +1,301 @@
|
|||||||
<script lang="ts">
|
|
||||||
// module-scoped so it survives client-side navigation —
|
|
||||||
// the intro should only ever play once per visit
|
|
||||||
let introPlayed = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: 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"
|
|
||||||
></feTurbulence>
|
|
||||||
<feColorMatrix type="saturate" values="0"></feColorMatrix>
|
|
||||||
<feBlend mode="multiply" in="SourceGraphic"></feBlend>
|
|
||||||
</filter>
|
|
||||||
</svg>
|
|
||||||
<div class="page-wrapper" ref="pageEl">
|
|
||||||
<header class="header">
|
|
||||||
<div class="header-upper">
|
|
||||||
<slot name="header-upper">
|
|
||||||
<div class="flex-center header-upper-nav">
|
|
||||||
<button
|
|
||||||
class="theme-toggle"
|
|
||||||
@click="toggleTheme"
|
|
||||||
v-if="!theme.unknown"
|
|
||||||
>
|
|
||||||
{{ theme.value === 'dark' ? '🌚' : '🌝' }}
|
|
||||||
</button>
|
|
||||||
<div class="flex-ff"></div>
|
|
||||||
<a
|
|
||||||
class="basic-link"
|
|
||||||
href="https://github.com/jaronpate"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<NuxtLink to="/writing" class="basic-link">
|
|
||||||
Writing
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<nav class="site-nav">
|
||||||
|
<NuxtLink to="/writing">Writing</NuxtLink>
|
||||||
|
<a href="https://github.com/jaronpate">GitHub</a>
|
||||||
|
<label class="theme-picker">
|
||||||
|
<span class="visually-hidden">Background</span>
|
||||||
|
<select
|
||||||
|
class="theme-select"
|
||||||
|
:value="id"
|
||||||
|
@change="onThemeChange"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="theme in themes"
|
||||||
|
:key="theme.id"
|
||||||
|
:value="theme.id"
|
||||||
|
>
|
||||||
|
{{ theme.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<!-- <label class="theme-picker">
|
||||||
|
<span class="visually-hidden">Font</span>
|
||||||
|
<select
|
||||||
|
class="theme-select"
|
||||||
|
:value="fontId"
|
||||||
|
@change="onFontChange"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="font in fonts"
|
||||||
|
:key="font.id"
|
||||||
|
:value="font.id"
|
||||||
|
>
|
||||||
|
{{ font.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label> -->
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
<h1 class="hero__title">{{ title }}</h1>
|
||||||
</div>
|
<p v-if="description" class="hero__sub">{{ description }}</p>
|
||||||
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<div class="page-body">
|
||||||
<slot />
|
<slot />
|
||||||
<div style="flex: 1"></div>
|
</div>
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="site-footer">
|
||||||
<div class="footer-upper flex-center">
|
<a href="mailto:yo@jp.wtf">yo@jp.wtf</a>
|
||||||
<p>
|
|
||||||
<a class="punch-link" href="mailto:yo@jp.wtf"
|
|
||||||
>Say hi.</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<div class="flex-ff"></div>
|
<div class="flex-ff"></div>
|
||||||
<p style="text-align: right">Let's make something cool.</p>
|
<span>Est. 2003</span>
|
||||||
</div>
|
<span>Austin, TX</span>
|
||||||
<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>
|
</footer>
|
||||||
</div>
|
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 2rem;
|
|
||||||
height: 8vh;
|
|
||||||
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 {
|
||||||
.page-wrapper {
|
--flare-length: 1.125rem;
|
||||||
--main-width: 100vw;
|
--flare-spread: 1.125rem;
|
||||||
padding: 0 1rem;
|
--flare-overhang: 1.5rem;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
width: var(--col);
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-left: var(--col-left);
|
||||||
|
margin-right: auto;
|
||||||
|
padding: calc(var(--flare-spread) + 0.45rem) 0 0.85rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: calc(-1 * var(--flare-overhang));
|
||||||
|
right: calc(-1 * var(--flare-overhang));
|
||||||
|
height: var(--flare-spread);
|
||||||
|
background-color: var(--muted);
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-mask:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
|
||||||
|
left center / var(--flare-length) 100% no-repeat,
|
||||||
|
linear-gradient(#000, #000) center /
|
||||||
|
calc(100% - var(--flare-length) * 1.55) 1px no-repeat,
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
|
||||||
|
right center / var(--flare-length) 100% no-repeat;
|
||||||
|
mask:
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M20 8H10M10 8C5 8 2.5 4.5 1 2M10 8C5 8 2.5 11.5 1 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
|
||||||
|
left center / var(--flare-length) 100% no-repeat,
|
||||||
|
linear-gradient(#000, #000) center /
|
||||||
|
calc(100% - var(--flare-length) * 1.55) 1px no-repeat,
|
||||||
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 16' preserveAspectRatio='none' fill='none'%3E%3Cpath d='M0 8H10M10 8C15 8 17.5 4.5 19 2M10 8C15 8 17.5 11.5 19 14' stroke='black' stroke-width='1' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E")
|
||||||
|
right center / var(--flare-length) 100% no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer a {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-ff {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.hero {
|
||||||
|
padding-bottom: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-lower,
|
.hero__nav {
|
||||||
.footer-upper {
|
flex-wrap: wrap;
|
||||||
border-radius: 12px;
|
margin-bottom: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-lower::before,
|
.hero__title {
|
||||||
.footer-upper::before {
|
font-size: 2rem;
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+44
-411
@@ -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,19 +30,11 @@ 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',
|
||||||
description:
|
description:
|
||||||
"I built many things I'm proud of at Edlink. Notably, I designed and shipped Edlink’s internal billing platform for ~150 clients, a flat file ingestion engine that enabled arbitrary data mapping, client-embedded UI widgets, user onboarding flows, and a secure sandbox for running untrusted code with a live data-preview service.",
|
"I built many things I'm proud of at Edlink. Notably, I designed and shipped the internal billing platform for ~150 clients, a flat file ingestion engine that enabled importing of arbitrary data, client-embedded UI widgets, user onboarding flows, and a secure sandbox for running untrusted code with a live data-preview service.",
|
||||||
link: 'https://ed.link',
|
link: 'https://ed.link',
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
@@ -186,117 +172,20 @@ useHead({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const photos = [
|
|
||||||
{
|
|
||||||
src: '/photography/athens_red_scooter.jpg',
|
|
||||||
alt: 'Red scooter in Athens',
|
|
||||||
location: 'Athens',
|
|
||||||
year: '2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/photography/athens_subway.jpg',
|
|
||||||
alt: 'Athens metro station',
|
|
||||||
location: 'Athens',
|
|
||||||
year: '2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/photography/athens_sunset.jpg',
|
|
||||||
alt: 'Athens at sunset',
|
|
||||||
location: 'Athens',
|
|
||||||
year: '2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/photography/london_porsche.jpg',
|
|
||||||
alt: 'Porsche shop in London',
|
|
||||||
location: 'London',
|
|
||||||
year: '2025',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/photography/underground.jpg',
|
|
||||||
alt: 'Entrace to the London underground',
|
|
||||||
location: 'London',
|
|
||||||
year: '2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/photography/austin_parking.jpg',
|
|
||||||
alt: 'Parking garage in Austin',
|
|
||||||
location: 'Austin',
|
|
||||||
year: '2024',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pageTitle = ref(titles[0]);
|
|
||||||
|
|
||||||
const rotateTitle = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const i = titles.findIndex((t) => t === pageTitle.value);
|
|
||||||
pageTitle.value = titles[i + 1] ?? titles[0] ?? 'Oops!';
|
|
||||||
rotateTitle();
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
definePageMeta({
|
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">
|
||||||
<section class="rows">
|
<article>
|
||||||
<h5>Selected Work</h5>
|
<h3>Selected Work</h3>
|
||||||
<div class="section__content">
|
<template v-for="work in selectedWorks" :key="work.title">
|
||||||
<a
|
<p>
|
||||||
v-for="work in selectedWorks"
|
<a :href="work.link" target="_blank" rel="noopener">{{
|
||||||
:key="work.title"
|
work.title
|
||||||
class="work"
|
}}</a>:
|
||||||
:href="work.link"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<h3 class="work__title">{{ work.title }}</h3>
|
|
||||||
<p class="work__description">
|
|
||||||
{{ work.description }}
|
{{ work.description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="work__images">
|
<div class="work__images">
|
||||||
@@ -308,34 +197,26 @@ onMounted(() => {
|
|||||||
<img :src="image.src" :alt="image.alt" />
|
<img :src="image.src" :alt="image.alt" />
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</template>
|
||||||
</div>
|
|
||||||
</section>
|
<h3>Projects</h3>
|
||||||
<section class="rows">
|
<ul>
|
||||||
<h5>Projects</h5>
|
<li v-for="project in projects" :key="project.href">
|
||||||
<div class="section__content">
|
<p>
|
||||||
<a
|
<a :href="project.href" target="_blank" rel="noopener">{{
|
||||||
v-for="project in projects"
|
project.title
|
||||||
:key="project.href"
|
}}</a>:
|
||||||
:href="project.href"
|
|
||||||
target="_blank"
|
|
||||||
class="project"
|
|
||||||
>
|
|
||||||
<h3 class="project__title">{{ project.title }}</h3>
|
|
||||||
<p class="project__description">
|
|
||||||
{{ project.description }}
|
{{ project.description }}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</section>
|
|
||||||
<section class="about">
|
<h3>About</h3>
|
||||||
<h5>About</h5>
|
|
||||||
<div class="section__content">
|
|
||||||
<p>
|
<p>
|
||||||
I left school, my job, and moved to Austin in 2022 to write
|
I left school, my job, and moved to Austin in 2022 to write code
|
||||||
code full time. That was definitely one of the best and
|
full time. That was definitely one of the best and most life
|
||||||
most life changing decisions I've ever made. I've learned
|
changing decisions I've ever made. I've learned a lot since then
|
||||||
a lot since then and <em>still</em> have much more to learn.
|
and <em>still</em> have much more to learn.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
These days I build things because I find joy in making something
|
These days I build things because I find joy in making something
|
||||||
@@ -343,155 +224,64 @@ onMounted(() => {
|
|||||||
level of polish that users don't even notice.
|
level of polish that users don't even notice.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
In a time where code is cheap I pride myself in taste and execution.
|
In a time where code is cheap I pride myself in taste and
|
||||||
The best sticking point — in my limited opinion so far — is the
|
execution. The best sticking point (in my limited opinion so far)
|
||||||
way using your product <em>feels</em>. So I want everything I make to
|
is the way using your product <em>feels</em>. So I want
|
||||||
simply <em>feel</em> the best to use.
|
everything I make to simply <em>feel</em> the best to use.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you want to build something cool or connect
|
If you want to build something cool or connect
|
||||||
<a class="basic-link" href="mailto:yo@jp.wtf">hit me up</a>
|
<a href="mailto:yo@jp.wtf">hit me up</a> — we'll grab a coffee or
|
||||||
— we'll grab a coffee or beer if you're in Austin! I
|
beer if you're in Austin! I still dont know enough people to form
|
||||||
still dont know enough people to form a band yet ;)
|
a band yet ;)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</article>
|
||||||
</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>
|
</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,161 +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) {
|
|
||||||
.photo-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,40 +12,76 @@ 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 { data: page } = await useAsyncData(route.path, () => {
|
const slug = computed(() =>
|
||||||
return queryCollection('writing')
|
Array.isArray(route.params.slug)
|
||||||
.path(route.path.replace('/writing/', ''))
|
? route.params.slug.join('/')
|
||||||
.first();
|
: String(route.params.slug ?? ''),
|
||||||
});
|
);
|
||||||
|
const contentPath = computed(() => '/' + slug.value);
|
||||||
|
const folderName = computed(
|
||||||
|
() => slug.value.split('/').filter(Boolean).at(-1) ?? slug.value,
|
||||||
|
);
|
||||||
|
|
||||||
if (!page.value) {
|
const { data } = await useAsyncData(
|
||||||
|
() => `writing:${route.path}`,
|
||||||
|
async () => {
|
||||||
|
const all = await queryCollection('writing').order('date', 'DESC').all();
|
||||||
|
const visible = all.filter((page) => import.meta.dev || !page.demo);
|
||||||
|
const article = visible.find(
|
||||||
|
(page) =>
|
||||||
|
page.path === contentPath.value || page.stem === slug.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (article) {
|
||||||
|
return { kind: 'article' as const, article, pages: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = contentPath.value.endsWith('/')
|
||||||
|
? contentPath.value
|
||||||
|
: `${contentPath.value}/`;
|
||||||
|
const pages = visible.filter((page) => page.path.startsWith(prefix));
|
||||||
|
|
||||||
|
if (!pages.length) return null;
|
||||||
|
return { kind: 'folder' as const, article: null, pages };
|
||||||
|
},
|
||||||
|
{ watch: [() => route.path] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data.value) {
|
||||||
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' });
|
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const articleTitle = page.value.title;
|
const page = computed(() => data.value?.article ?? null);
|
||||||
const articleDescription = page.value.description;
|
const isFolder = computed(() => data.value?.kind === 'folder');
|
||||||
|
const articleTitle = page.value?.title ?? folderName.value;
|
||||||
|
const articleDescription = page.value?.description ?? '';
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: articleTitle,
|
title: articleTitle,
|
||||||
ogTitle: articleTitle,
|
ogTitle: articleTitle,
|
||||||
description: articleDescription,
|
description: articleDescription || articleTitle,
|
||||||
ogDescription: articleDescription,
|
ogDescription: articleDescription || articleTitle,
|
||||||
ogUrl: articleUrl,
|
ogUrl: articleUrl,
|
||||||
ogType: 'article',
|
ogType: isFolder.value ? 'website' : 'article',
|
||||||
ogImage: DEFAULT_IMAGE,
|
ogImage: DEFAULT_IMAGE,
|
||||||
ogImageAlt: articleTitle,
|
ogImageAlt: articleTitle,
|
||||||
twitterCard: 'summary',
|
twitterCard: 'summary',
|
||||||
twitterTitle: articleTitle,
|
twitterTitle: articleTitle,
|
||||||
twitterDescription: articleDescription,
|
twitterDescription: articleDescription || articleTitle,
|
||||||
twitterImage: DEFAULT_IMAGE,
|
twitterImage: DEFAULT_IMAGE,
|
||||||
articlePublishedTime: new Date(page.value.date).toISOString(),
|
articlePublishedTime: page.value
|
||||||
articleModifiedTime: new Date(page.value.date).toISOString(),
|
? new Date(page.value.date).toISOString()
|
||||||
articleAuthor: [SITE_NAME],
|
: undefined,
|
||||||
|
articleModifiedTime: page.value
|
||||||
|
? new Date(page.value.date).toISOString()
|
||||||
|
: undefined,
|
||||||
|
articleAuthor: page.value ? [SITE_NAME] : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
link: [{ rel: 'canonical', href: articleUrl }],
|
link: [{ rel: 'canonical', href: articleUrl }],
|
||||||
script: [
|
script: page.value
|
||||||
|
? [
|
||||||
{
|
{
|
||||||
key: 'json-ld-article',
|
key: 'json-ld-article',
|
||||||
type: 'application/ld+json',
|
type: 'application/ld+json',
|
||||||
@@ -82,8 +118,12 @@ useHead({
|
|||||||
url: articleUrl,
|
url: articleUrl,
|
||||||
headline: articleTitle,
|
headline: articleTitle,
|
||||||
description: articleDescription,
|
description: articleDescription,
|
||||||
datePublished: new Date(page.value.date).toISOString(),
|
datePublished: new Date(
|
||||||
dateModified: new Date(page.value.date).toISOString(),
|
page.value.date,
|
||||||
|
).toISOString(),
|
||||||
|
dateModified: new Date(
|
||||||
|
page.value.date,
|
||||||
|
).toISOString(),
|
||||||
image: {
|
image: {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
'@id': `${articleUrl}#article-image`,
|
'@id': `${articleUrl}#article-image`,
|
||||||
@@ -131,55 +171,40 @@ useHead({
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
|
: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatDate = (date: Date | string) => {
|
||||||
|
return new Date(date).toLocaleDateString('en-US', {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<NuxtLayout
|
<NuxtLayout
|
||||||
name="main"
|
name="main"
|
||||||
:title="page?.title"
|
:title="page ? page.title : folderName"
|
||||||
:description="page?.description"
|
:description="page ? formatDate(page.date) : `Writing from ${folderName}`"
|
||||||
>
|
>
|
||||||
<template #header-upper>
|
<article v-if="page">
|
||||||
<div class="header-upper-inner">
|
<div class="article-body">
|
||||||
<NuxtLink
|
<p v-if="page.description">
|
||||||
to="/writing"
|
<strong>{{ page.description }}</strong>
|
||||||
class="back-link"
|
</p>
|
||||||
aria-label="Back to writing"
|
<ContentRenderer :value="page" />
|
||||||
>
|
|
||||||
<span class="arrow">←</span>
|
|
||||||
<span>Back</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</article>
|
||||||
<ContentRenderer
|
<article v-else>
|
||||||
class="writing__content"
|
<WritingList :pages="data?.pages ?? []" />
|
||||||
v-if="page"
|
</article>
|
||||||
:value="page"
|
|
||||||
/>
|
|
||||||
</NuxtLayout>
|
</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>
|
|
||||||
|
|||||||
+10
-117
@@ -7,9 +7,11 @@ import {
|
|||||||
websiteId,
|
websiteId,
|
||||||
} from '~/utils/site';
|
} from '~/utils/site';
|
||||||
|
|
||||||
// const { data: pages } = await useAsyncData('writing-pages', () =>
|
// TODO: Uncomment when ready to show writing
|
||||||
// queryCollection('writing').order('date', 'DESC').all(),
|
// const { data: pages } = await useAsyncData('writing-pages', async () => {
|
||||||
// );
|
// const all = await queryCollection('writing').order('date', 'DESC').all();
|
||||||
|
// return import.meta.dev ? all : all.filter((page) => !page.demo);
|
||||||
|
// });
|
||||||
|
|
||||||
const pages = ref<any[]>([]);
|
const pages = ref<any[]>([]);
|
||||||
|
|
||||||
@@ -74,7 +76,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 +87,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,128 +116,19 @@ useHead({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
// ex. May 5, 2026
|
|
||||||
return `${date.toLocaleDateString('en-US', { month: 'short' })} ${date.getDate()}, ${date.getFullYear()}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
});
|
});
|
||||||
</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."
|
||||||
>
|
>
|
||||||
<template #header-upper>
|
<article>
|
||||||
<div class="header-upper-inner">
|
<WritingList :pages="pages ?? []" />
|
||||||
<NuxtLink
|
</article>
|
||||||
to="/"
|
|
||||||
class="back-link"
|
|
||||||
aria-label="Back to Home"
|
|
||||||
>
|
|
||||||
<span class="arrow">←</span>
|
|
||||||
<span>Home</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<section>
|
|
||||||
<div class="section__content">
|
|
||||||
<template v-if="pages?.length">
|
|
||||||
<NuxtLink
|
|
||||||
v-for="page in pages"
|
|
||||||
:to="'/writing/' + page.path"
|
|
||||||
:key="page.path"
|
|
||||||
class="article"
|
|
||||||
>
|
|
||||||
<h3 class="article__title">{{ page.title }}</h3>
|
|
||||||
<!-- <p class="article__date">
|
|
||||||
{{ formatDate(page.date) }}
|
|
||||||
</p> -->
|
|
||||||
<p class="article__description">
|
|
||||||
{{ page.description }}
|
|
||||||
</p>
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="empty-state">
|
|
||||||
<p>No thoughts...head empty</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.header-upper-inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section > .section__content {
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section:not(:has(h5)) > .section__content {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article {
|
|
||||||
border-bottom: 1px solid var(--muted);
|
|
||||||
padding: 1rem 0;
|
|
||||||
display: block;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article:first-of-type {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article:last-of-type {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article:hover > .article__title {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 4px;
|
|
||||||
font-style: italic;
|
|
||||||
font-family: var(--font-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article__title {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article__date {
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: small;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article__description {
|
|
||||||
margin: 0;
|
|
||||||
font-size: small;
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { gsap } from 'gsap';
|
|
||||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
gsap,
|
|
||||||
ScrollTrigger,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export const BODY_FONTS = [
|
||||||
|
{ id: 'plantin', label: 'Plantin' },
|
||||||
|
{ id: 'palatino', label: 'Palatino' },
|
||||||
|
{ id: 'redaction', label: 'Redaction' },
|
||||||
|
// These are to be used by components or other styles
|
||||||
|
// { id: 'redaction-10', label: 'Redaction 10' },
|
||||||
|
// { id: 'redaction-20', label: 'Redaction 20' },
|
||||||
|
// { id: 'redaction-35', label: 'Redaction 35' },
|
||||||
|
// { id: 'redaction-50', label: 'Redaction 50' },
|
||||||
|
// { id: 'redaction-70', label: 'Redaction 70' },
|
||||||
|
// { id: 'redaction-100', label: 'Redaction 100' },
|
||||||
|
{ id: 'sohne', label: 'Sohne' },
|
||||||
|
{ id: 'georgia', label: 'Georgia' },
|
||||||
|
{ id: 'system', label: 'System Sans' },
|
||||||
|
{ id: 'space', label: 'Space Mono' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type BodyFontId = (typeof BODY_FONTS)[number]['id'];
|
||||||
|
|
||||||
|
export const DEFAULT_BODY_FONT: BodyFontId = 'plantin';
|
||||||
|
export const BODY_FONT_STORAGE_KEY = 'body-font';
|
||||||
|
|
||||||
|
export function isBodyFontId(value: string | null): value is BodyFontId {
|
||||||
|
return BODY_FONTS.some((font) => font.id === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REDACTION_STRENGTHS = [0, 10, 20, 35, 50, 70, 100] as const;
|
||||||
|
|
||||||
|
export type RedactionStrength = (typeof REDACTION_STRENGTHS)[number];
|
||||||
|
|
||||||
|
export function redactionFamily(strength: RedactionStrength): string {
|
||||||
|
return strength === 0 ? 'Redaction' : `Redaction ${strength}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRedactionStrength(value: unknown): RedactionStrength {
|
||||||
|
const n = Number(value);
|
||||||
|
if (REDACTION_STRENGTHS.includes(n as RedactionStrength)) {
|
||||||
|
return n as RedactionStrength;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(n)) return 35;
|
||||||
|
return REDACTION_STRENGTHS.reduce((best, grade) =>
|
||||||
|
Math.abs(grade - n) < Math.abs(best - n) ? grade : best,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
const WIDE = '(min-width: 1200px)';
|
||||||
|
const GAP = 8;
|
||||||
|
|
||||||
|
export function stackMarginNotes() {
|
||||||
|
if (!import.meta.client) return;
|
||||||
|
|
||||||
|
const notes = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.margin-note'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!window.matchMedia(WIDE).matches) {
|
||||||
|
for (const note of notes) note.style.top = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const note of notes) note.style.top = '';
|
||||||
|
|
||||||
|
let previousBottom = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const article = note.closest('article');
|
||||||
|
const mark = note
|
||||||
|
.closest('.margin-note-root')
|
||||||
|
?.querySelector<HTMLElement>('.margin-note-mark');
|
||||||
|
if (!article || !mark) continue;
|
||||||
|
|
||||||
|
const articleTop = article.getBoundingClientRect().top;
|
||||||
|
let top = mark.getBoundingClientRect().top - articleTop;
|
||||||
|
if (top < previousBottom + GAP) {
|
||||||
|
top = previousBottom + GAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
note.style.top = `${Math.max(0, top)}px`;
|
||||||
|
previousBottom = top + note.offsetHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let listening = false;
|
||||||
|
|
||||||
|
export function watchMarginNotes() {
|
||||||
|
if (!import.meta.client || listening) {
|
||||||
|
stackMarginNotes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listening = true;
|
||||||
|
const run = () => stackMarginNotes();
|
||||||
|
|
||||||
|
window.addEventListener('resize', run);
|
||||||
|
document.fonts?.ready.then(run);
|
||||||
|
new ResizeObserver(run).observe(document.documentElement);
|
||||||
|
new MutationObserver(run).observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
});
|
||||||
|
run();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export default defineContentConfig({
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
date: z.coerce.date(),
|
date: z.coerce.date(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
demo: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: 'hot take: frontend developers are underpaid.'
|
title: 'hot take: frontend developers are underpaid.'
|
||||||
date: 2026-05-03
|
date: 2026-09-22
|
||||||
description: that shit is hard
|
description: that shit is hard
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
title: Notes in the margin
|
||||||
|
date: 2026-08-22
|
||||||
|
description: Every mark this site knows how to make — body type, asides, notices, redaction, quotes, lists, code, and images.
|
||||||
|
demo: true
|
||||||
|
---
|
||||||
|
|
||||||
|
This page is a working sample, not an essay. Walk it when you change type, color, or a content component. The **description** above is the article lead — bold, then the body.
|
||||||
|
|
||||||
|
Ordinary copy sits in the column. You can *emphasize*, **stress**, or [link out](https://jaronpate.com). A thought that does not belong in the sentence can sit beside it. :margin-note[(A margin note. Wide screens put it in the right gutter. Narrow screens drop it under the paragraph.)] Parentheticals you can actually skip.
|
||||||
|
|
||||||
|
## Headings
|
||||||
|
|
||||||
|
`h2` and `h3` share one italic, muted style. Use them as section marks, not display type.
|
||||||
|
|
||||||
|
### A third-level mark
|
||||||
|
|
||||||
|
Still the same size and color. Hierarchy is the word, not the weight.
|
||||||
|
|
||||||
|
## Margin notes
|
||||||
|
|
||||||
|
Two notes in one stretch of text, so you can see them stack.
|
||||||
|
|
||||||
|
The first is a mutter. :margin-note[(First note — should sit next to this sentence.)] A little later, a second one arrives. :margin-note[(Second note. On a wide screen these should not collide with the column.)] Between them the column keeps moving.
|
||||||
|
|
||||||
|
```md
|
||||||
|
:margin-note[(like this)]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
A notice interrupts on purpose. It stays in the column.
|
||||||
|
|
||||||
|
::notice
|
||||||
|
**In-column.** Use this when the aside *is* the point for a minute. Navy, hard to miss, still inside the measure.
|
||||||
|
::
|
||||||
|
|
||||||
|
```md
|
||||||
|
::notice
|
||||||
|
**In-column.**
|
||||||
|
::
|
||||||
|
```
|
||||||
|
|
||||||
|
And when it needs to break the column:
|
||||||
|
|
||||||
|
::notice{bleed}
|
||||||
|
**Bleed.** Same idea, edge to edge. Save it for the one time in a piece you actually want to shout.
|
||||||
|
::
|
||||||
|
|
||||||
|
```md
|
||||||
|
::notice{bleed}
|
||||||
|
**Bleed.**
|
||||||
|
::
|
||||||
|
```
|
||||||
|
|
||||||
|
## Redaction
|
||||||
|
|
||||||
|
A span can be set in Redaction. `strength` is a grade: `0` is the clean face, `100` is almost gone. Other numbers snap to `0`, `10`, `20`, `35`, `50`, `70`, or `100`. Default is `100`.
|
||||||
|
|
||||||
|
```md
|
||||||
|
:redacted[these words]{strength="50"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same phrase, every grade:
|
||||||
|
|
||||||
|
- 0 — :redacted[the record is still intact]{strength="0"}
|
||||||
|
- 10 — :redacted[the record is still intact]{strength="10"}
|
||||||
|
- 20 — :redacted[the record is still intact]{strength="20"}
|
||||||
|
- 35 — :redacted[the record is still intact]{strength="35"}
|
||||||
|
- 50 — :redacted[the record is still intact]{strength="50"}
|
||||||
|
- 70 — :redacted[the record is still intact]{strength="70"}
|
||||||
|
- 100 — :redacted[the record is still intact]{strength="100"}
|
||||||
|
|
||||||
|
Inline, mixed into a sentence: the filing names :redacted[a person we will not repeat]{strength="20"}, then later :redacted[almost nothing remains]{strength="100"}.
|
||||||
|
|
||||||
|
**Bold around** :redacted[a redacted span]{strength="50"} should pick Redaction Bold.
|
||||||
|
|
||||||
|
## Blockquote
|
||||||
|
|
||||||
|
The pointing hand sits in the gutter. Use it for a quote, or a line you want pointed at.
|
||||||
|
|
||||||
|
> The best sticking point is the way using the thing *feels*. Everything else is infrastructure.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Unordered, for a pile of equals:
|
||||||
|
|
||||||
|
- Plantin for reading
|
||||||
|
- Space Mono for the wordmark, footer, and code
|
||||||
|
- Redaction only where you ask for it
|
||||||
|
|
||||||
|
Ordered, when sequence matters:
|
||||||
|
|
||||||
|
1. Write the sentence
|
||||||
|
2. Park the mutter in a margin note
|
||||||
|
3. Redact what should stay almost-said
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline: the helper is `resolveRedactionStrength`, and the shortcode is `:redacted`.
|
||||||
|
|
||||||
|
A block:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const grade = resolveRedactionStrength(strength);
|
||||||
|
const family = redactionFamily(grade);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|
A figure in the column. This is an old Edlink shot, just to see the matte and radius.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
That’s the set. Margin for a mutter. Notice for a point. Redaction when the words should still be there, barely. Everything else is ordinary prose.
|
||||||
+22
-6
@@ -27,10 +27,22 @@ export default defineNuxtConfig({
|
|||||||
type: 'd1',
|
type: 'd1',
|
||||||
bindingName: 'DB',
|
bindingName: 'DB',
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
markdown: {
|
||||||
|
highlight: {
|
||||||
|
theme: {
|
||||||
|
default: 'github-light',
|
||||||
|
'light-mode': 'github-light',
|
||||||
|
'dark-mode': 'github-dark',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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 +56,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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user