Set up tailwind and nano theme

This commit is contained in:
ryan 2026-01-05 13:21:19 +11:00
parent 5e5d6166d5
commit a5c1eaedc7
21 changed files with 1180 additions and 20 deletions

View File

@ -2,14 +2,19 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import node from '@astrojs/node';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
site: 'https://ryanpandya.com',
integrations: [mdx()],
adapter: node({
mode: 'standalone'
})
}),
vite: {
plugins: [tailwindcss()],
},
});

View File

@ -11,6 +11,14 @@
"dependencies": {
"@astrojs/mdx": "^4.3.13",
"@astrojs/node": "^9.5.1",
"astro": "^5.16.6"
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@fontsource/martel": "^5.2.8",
"@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.6",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18"
}
}

458
pnpm-lock.yaml generated
View File

@ -10,16 +10,44 @@ importers:
dependencies:
'@astrojs/mdx':
specifier: ^4.3.13
version: 4.3.13(astro@5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3))
version: 4.3.13(astro@5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2))
'@astrojs/node':
specifier: ^9.5.1
version: 9.5.1(astro@5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3))
version: 9.5.1(astro@5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2))
'@fontsource-variable/ibm-plex-sans':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/martel':
specifier: ^5.2.8
version: 5.2.8
'@tailwindcss/postcss':
specifier: ^4.1.18
version: 4.1.18
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.1.18)
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))
astro:
specifier: ^5.16.6
version: 5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3)
version: 5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2)
clsx:
specifier: ^2.1.1
version: 2.1.1
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
tailwindcss:
specifier: ^4.1.18
version: 4.1.18
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@astrojs/compiler@2.13.0':
resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==}
@ -228,6 +256,12 @@ packages:
cpu: [x64]
os: [win32]
'@fontsource-variable/ibm-plex-sans@5.2.8':
resolution: {integrity: sha512-n5PF2iFa0CZT0QYTPzxvZ39opC9LnU0zdoRccoADbs+Dtsd+lbXOZF7RNuIPHcQX1dKjF63sxnRImQIB5eD0Ag==}
'@fontsource/martel@5.2.8':
resolution: {integrity: sha512-5leWG3FVh2BS+4TfVYoaUoDGizhjmR98e0j2UpalfYu008Oe5s5XgIqhXCDCqaWBtXmZOb2I1Q9S47YLiXGEiQ==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
@ -365,9 +399,22 @@ packages:
cpu: [x64]
os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
@ -517,6 +564,104 @@ packages:
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
'@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
'@tailwindcss/oxide-android-arm64@4.1.18':
resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.1.18':
resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.1.18':
resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.1.18':
resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.1.18':
resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
engines: {node: '>= 10'}
'@tailwindcss/postcss@4.1.18':
resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tailwindcss/vite@4.1.18':
resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@ -797,6 +942,10 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
enhanced-resolve@5.18.4:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@ -898,6 +1047,9 @@ packages:
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
@ -996,6 +1148,10 @@ packages:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
@ -1004,6 +1160,76 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.30.2:
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.2:
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.2:
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.2:
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.2:
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.2:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@ -1282,6 +1508,10 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@ -1462,6 +1692,16 @@ packages:
engines: {node: '>=16'}
hasBin: true
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
tailwindcss@4.1.18:
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@ -1621,6 +1861,9 @@ packages:
uploadthing:
optional: true
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@ -1696,6 +1939,11 @@ packages:
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@ -1731,6 +1979,8 @@ packages:
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@astrojs/compiler@2.13.0': {}
'@astrojs/internal-helpers@0.7.5': {}
@ -1761,12 +2011,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@astrojs/mdx@4.3.13(astro@5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3))':
'@astrojs/mdx@4.3.13(astro@5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2))':
dependencies:
'@astrojs/markdown-remark': 6.3.10
'@mdx-js/mdx': 3.1.1
acorn: 8.15.0
astro: 5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3)
astro: 5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2)
es-module-lexer: 1.7.0
estree-util-visit: 2.0.0
hast-util-to-html: 9.0.5
@ -1780,10 +2030,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@astrojs/node@9.5.1(astro@5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3))':
'@astrojs/node@9.5.1(astro@5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2))':
dependencies:
'@astrojs/internal-helpers': 0.7.5
astro: 5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3)
astro: 5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2)
send: 1.2.1
server-destroy: 1.0.1
transitivePeerDependencies:
@ -1905,6 +2155,10 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
'@fontsource-variable/ibm-plex-sans@5.2.8': {}
'@fontsource/martel@5.2.8': {}
'@img/colour@1.0.0':
optional: true
@ -2002,8 +2256,25 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mdx-js/mdx@3.1.1':
dependencies:
'@types/estree': 1.0.8
@ -2147,6 +2418,87 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tailwindcss/node@4.1.18':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.18.4
jiti: 2.6.1
lightningcss: 1.30.2
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.1.18
'@tailwindcss/oxide-android-arm64@4.1.18':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.1.18':
optional: true
'@tailwindcss/oxide-darwin-x64@4.1.18':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.1.18':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.1.18':
optional: true
'@tailwindcss/oxide@4.1.18':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.18
'@tailwindcss/oxide-darwin-arm64': 4.1.18
'@tailwindcss/oxide-darwin-x64': 4.1.18
'@tailwindcss/oxide-freebsd-x64': 4.1.18
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
'@tailwindcss/oxide-linux-arm64-musl': 4.1.18
'@tailwindcss/oxide-linux-x64-gnu': 4.1.18
'@tailwindcss/oxide-linux-x64-musl': 4.1.18
'@tailwindcss/oxide-wasm32-wasi': 4.1.18
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
'@tailwindcss/postcss@4.1.18':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18
postcss: 8.5.6
tailwindcss: 4.1.18
'@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.18
'@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))':
dependencies:
'@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18
tailwindcss: 4.1.18
vite: 6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@ -2216,7 +2568,7 @@ snapshots:
astring@1.9.0: {}
astro@5.16.6(@types/node@25.0.3)(rollup@4.54.0)(typescript@5.9.3):
astro@5.16.6(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(typescript@5.9.3)(yaml@2.8.2):
dependencies:
'@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5
@ -2273,8 +2625,8 @@ snapshots:
unist-util-visit: 5.0.0
unstorage: 1.17.3
vfile: 6.0.3
vite: 6.4.1(@types/node@25.0.3)
vitefu: 1.1.1(vite@6.4.1(@types/node@25.0.3))
vite: 6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
vitefu: 1.1.1(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
@ -2427,8 +2779,7 @@ snapshots:
destr@2.0.5: {}
detect-libc@2.1.2:
optional: true
detect-libc@2.1.2: {}
deterministic-object-hash@2.0.2:
dependencies:
@ -2474,6 +2825,11 @@ snapshots:
encodeurl@2.0.0: {}
enhanced-resolve@5.18.4:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
entities@4.5.0: {}
entities@6.0.1: {}
@ -2602,6 +2958,8 @@ snapshots:
github-slugger@2.0.0: {}
graceful-fs@4.2.11: {}
h3@1.15.4:
dependencies:
cookie-es: 1.2.2
@ -2789,12 +3147,63 @@ snapshots:
dependencies:
is-inside-container: 1.0.0
jiti@2.6.1: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
kleur@3.0.3: {}
lightningcss-android-arm64@1.30.2:
optional: true
lightningcss-darwin-arm64@1.30.2:
optional: true
lightningcss-darwin-x64@1.30.2:
optional: true
lightningcss-freebsd-x64@1.30.2:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.2:
optional: true
lightningcss-linux-arm64-gnu@1.30.2:
optional: true
lightningcss-linux-arm64-musl@1.30.2:
optional: true
lightningcss-linux-x64-gnu@1.30.2:
optional: true
lightningcss-linux-x64-musl@1.30.2:
optional: true
lightningcss-win32-arm64-msvc@1.30.2:
optional: true
lightningcss-win32-x64-msvc@1.30.2:
optional: true
lightningcss@1.30.2:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.30.2
lightningcss-darwin-arm64: 1.30.2
lightningcss-darwin-x64: 1.30.2
lightningcss-freebsd-x64: 1.30.2
lightningcss-linux-arm-gnueabihf: 1.30.2
lightningcss-linux-arm64-gnu: 1.30.2
lightningcss-linux-arm64-musl: 1.30.2
lightningcss-linux-x64-gnu: 1.30.2
lightningcss-linux-x64-musl: 1.30.2
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
longest-streak@3.1.0: {}
lru-cache@10.4.3: {}
@ -3344,6 +3753,11 @@ snapshots:
picomatch@4.0.3: {}
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@ -3662,6 +4076,12 @@ snapshots:
picocolors: 1.1.1
sax: 1.4.3
tailwind-merge@3.4.0: {}
tailwindcss@4.1.18: {}
tapable@2.3.0: {}
tiny-inflate@1.0.3: {}
tinyexec@1.0.2: {}
@ -3778,6 +4198,8 @@ snapshots:
ofetch: 1.5.1
ufo: 1.6.1
util-deprecate@1.0.2: {}
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
@ -3793,7 +4215,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@6.4.1(@types/node@25.0.3):
vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@ -3804,10 +4226,13 @@ snapshots:
optionalDependencies:
'@types/node': 25.0.3
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
yaml: 2.8.2
vitefu@1.1.1(vite@6.4.1(@types/node@25.0.3)):
vitefu@1.1.1(vite@6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)):
optionalDependencies:
vite: 6.4.1(@types/node@25.0.3)
vite: 6.4.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)
web-namespaces@2.0.1: {}
@ -3825,6 +4250,9 @@ snapshots:
xxhash-wasm@1.1.0: {}
yaml@2.8.2:
optional: true
yargs-parser@21.1.1: {}
yocto-queue@1.2.2: {}

View File

@ -0,0 +1,27 @@
---
import type { CollectionEntry } from "astro:content";
type Props = {
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
}
const { entry } = Astro.props;
---
<a href={`/${entry.collection}/${entry.slug}`} class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<div class="flex flex-col flex-1 truncate">
<div class="font-semibold">
{entry.data.title}
</div>
<div class="text-sm">
{entry.data.description}
</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute top-1/2 right-2 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current">
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-3 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
<polyline points="12 5 19 12 12 19" class="-translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
</svg>
</a>

View File

@ -0,0 +1,20 @@
---
type Props = {
href: string;
}
const { href } = Astro.props;
---
<a href={href} class="relative group w-fit flex pl-7 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current">
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
</svg>
<div class="text-sm">
<slot/>
</div>
</a>

View File

@ -0,0 +1,12 @@
<button id="back-to-top" class="relative group w-fit flex pl-8 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current rotate-90">
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
</svg>
<div class="text-sm">
Back to top
</div>
</button>

View File

@ -0,0 +1,7 @@
---
---
<div class="mx-auto max-w-screen-sm px-5">
<slot />
</div>

View File

@ -0,0 +1,93 @@
---
import Container from "@components/Container.astro";
import { SITE } from "@consts";
import BackToTop from "@components/BackToTop.astro";
---
<footer class="animate">
<Container>
<div class="relative">
<div class="absolute right-0 -top-20">
<BackToTop />
</div>
</div>
<div class="flex justify-between items-center">
<div>
&copy; {new Date().getFullYear()} &middot;
{SITE.NAME}
</div>
<div class="flex flex-wrap gap-1 items-center">
<button
id="light-theme-button"
aria-label="Light theme"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
>
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<button
id="dark-theme-button"
aria-label="Dark theme"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<button
id="system-theme-button"
aria-label="System theme"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</button>
</div>
</div>
</Container>
</footer>

View File

@ -0,0 +1,17 @@
---
interface Props {
date: Date;
}
const { date } = Astro.props;
---
<time datetime={date.toISOString()}>
{
date.toLocaleDateString("en-us", {
month: "short",
day: "numeric",
year: "numeric",
})
}
</time>

198
src/components/Head.astro Normal file
View File

@ -0,0 +1,198 @@
---
import "../styles/global.css";
import "@fontsource/martel/latin-400.css";
import "@fontsource/martel/latin-600.css";
import "@fontsource-variable/ibm-plex-sans";
// import martel400 from "@fontsource/martel/files/martel-latin-400-normal.woff2";
// import martel600 from "@fontsource/martel/files/martel-latin-600-normal.woff2";
// import plex from "@fontsource/ibm-plex-sans/files/ibm-plex-sans-latin-400-normal.woff2";
import { ClientRouter } from "astro:transitions";
import { SITE } from "@consts";
interface Props {
title: string;
description: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = "/nano.png" } = Astro.props;
---
<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link
rel="icon"
type="image/svg+xml"
href="/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
type="image/svg+xml"
href="/favicon-light.svg"
media="(prefers-color-scheme: light)"
/>
<link rel="icon" type="image/x-icon" href="/favicon-light.svg" />
<meta name="generator" content={Astro.generator} />
<!-- Font preloads -->
<!-- <link rel="preload" href={martel400} as="font" type="font/woff2" crossorigin />
<link rel="preload" href={martel600} as="font" type="font/woff2" crossorigin />
<link rel="preload" href={plex400} as="font" type="font/woff2" crossorigin />
<link rel="preload" href={plex600} as="font" type="font/woff2" crossorigin /> -->
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />
<ClientRouter />
<!-- RSS Link -->
<link
rel="alternate"
type="application/rss+xml"
title={SITE.NAME}
href={new URL("rss.xml", Astro.site)}
/>
<script>
import type { TransitionBeforeSwapEvent } from "astro:transitions/client";
document.addEventListener("astro:before-swap", (e) =>
[
...(e as TransitionBeforeSwapEvent).newDocument.head.querySelectorAll(
'link[as="font"]'
),
].forEach((link) => link.remove())
);
</script>
<script is:inline>
function init() {
preloadTheme();
onScroll();
animate();
const backToTop = document.getElementById("back-to-top");
backToTop?.addEventListener("click", (event) => scrollToTop(event));
const backToPrev = document.getElementById("back-to-prev");
backToPrev?.addEventListener("click", () => window.history.back());
const lightThemeButton = document.getElementById("light-theme-button");
lightThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "light");
toggleTheme(false);
});
const darkThemeButton = document.getElementById("dark-theme-button");
darkThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "dark");
toggleTheme(true);
});
const systemThemeButton = document.getElementById("system-theme-button");
systemThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "system");
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
if (localStorage.theme === "system" || !localStorage.theme) {
toggleTheme(event.matches);
}
});
document.addEventListener("scroll", onScroll);
}
function animate() {
const animateElements = document.querySelectorAll(".animate");
animateElements.forEach((element, index) => {
setTimeout(() => {
element.classList.add("show");
}, index * 150);
});
}
function onScroll() {
if (window.scrollY > 0) {
document.documentElement.classList.add("scrolled");
} else {
document.documentElement.classList.remove("scrolled");
}
}
function scrollToTop(event) {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
function toggleTheme(dark) {
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`* {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
-ms-transition: none !important;
transition: none !important;
}
`
)
);
document.head.appendChild(css);
if (dark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
window.getComputedStyle(css).opacity;
document.head.removeChild(css);
}
function preloadTheme() {
const userTheme = localStorage.theme;
if (userTheme === "light" || userTheme === "dark") {
toggleTheme(userTheme === "dark");
} else {
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
}
}
document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());
preloadTheme();
</script>

View File

@ -0,0 +1,34 @@
---
import Container from "@components/Container.astro";
import Link from "@components/Link.astro";
import { SITE } from "@consts";
---
<header>
<Container>
<div class="flex flex-wrap gap-y-2 justify-between">
<Link href="/" underline={false}>
<div class="font-semibold">
{SITE.NAME}
</div>
</Link>
<nav class="flex gap-1">
<Link href="/blog">
blog
</Link>
<span>
{`/`}
</span>
<Link href="/work">
work
</Link>
<span>
{`/`}
</span>
<Link href="/projects">
projects
</Link>
</nav>
</div>
</Container>
</header>

19
src/components/Link.astro Normal file
View File

@ -0,0 +1,19 @@
---
import { cn } from "@lib/utils";
type Props = {
href: string;
external?: boolean;
underline?: boolean;
}
const { href, external, underline = true, ...rest } = Astro.props;
---
<a
href={href}
target={ external ? "_blank" : "_self" }
class={cn("inline-block decoration-black/15 dark:decoration-white/30 hover:decoration-black/25 hover:dark:decoration-white/50 text-current hover:text-black hover:dark:text-white transition-colors duration-300 ease-in-out", underline && "underline underline-offset-2")}
{...rest}>
<slot/>
</a>

44
src/consts.ts Normal file
View File

@ -0,0 +1,44 @@
import type { Site, Metadata, Socials } from "@types";
export const SITE: Site = {
NAME: "Ryan Pandya",
EMAIL: "ryan@ryanpandya.com",
NUM_POSTS_ON_HOMEPAGE: 3,
NUM_WORKS_ON_HOMEPAGE: 2,
NUM_PROJECTS_ON_HOMEPAGE: 3,
};
export const HOME: Metadata = {
TITLE: "Home",
DESCRIPTION: "Astro Nano is a minimal and lightweight blog and portfolio.",
};
export const BLOG: Metadata = {
TITLE: "Blog",
DESCRIPTION: "A collection of articles on topics I am passionate about.",
};
export const WORK: Metadata = {
TITLE: "Work",
DESCRIPTION: "Where I have worked and what I have done.",
};
export const PROJECTS: Metadata = {
TITLE: "Projects",
DESCRIPTION: "A collection of my projects, with links to repositories and demos.",
};
export const SOCIALS: Socials = [
{
NAME: "twitter-x",
HREF: "https://twitter.com/markhorn_dev",
},
{
NAME: "github",
HREF: "https://github.com/markhorn-dev"
},
{
NAME: "linkedin",
HREF: "https://www.linkedin.com/in/markhorn-dev",
}
];

13
src/content/config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
draft: z.boolean().optional(),
}),
});
export const collections = { blog };

View File

@ -0,0 +1,27 @@
---
import Head from "@components/Head.astro";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import { SITE } from "@consts";
type Props = {
title: string;
description: string;
};
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<Head title={`${title} | ${SITE.NAME}`} description={description} />
</head>
<body>
<Header />
<main>
<slot />
</main>
<Footer />
</body>
</html>

40
src/lib/utils.ts Normal file
View File

@ -0,0 +1,40 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: Date) {
return Intl.DateTimeFormat("en-US", {
month: "short",
day: "2-digit",
year: "numeric"
}).format(date);
}
export function readingTime(html: string) {
const textOnly = html.replace(/<[^>]+>/g, "");
const wordCount = textOnly.split(/\s+/).length;
const readingTimeMinutes = ((wordCount / 200) + 1).toFixed();
return `${readingTimeMinutes} min read`;
}
export function dateRange(startDate: Date, endDate?: Date | string): string {
const startMonth = startDate.toLocaleString("default", { month: "short" });
const startYear = startDate.getFullYear().toString();
let endMonth;
let endYear;
if (endDate) {
if (typeof endDate === "string") {
endMonth = "";
endYear = endDate;
} else {
endMonth = endDate.toLocaleString("default", { month: "short" });
endYear = endDate.getFullYear().toString();
}
}
return `${startMonth}${startYear} - ${endMonth}${endYear}`;
}

View File

@ -0,0 +1,52 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import Container from "@components/Container.astro";
import ArrowCard from "@components/ArrowCard.astro";
import { BLOG } from "@consts";
const data = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
type Acc = {
[year: string]: CollectionEntry<"blog">[];
};
const posts = data.reduce((acc: Acc, post) => {
const year = post.data.date.getFullYear().toString();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
}, {});
const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a));
---
<PageLayout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
<Container>
<div class="space-y-10">
<div class="animate font-semibold text-black dark:text-white">Blog</div>
<div class="space-y-4">
{
years.map((year) => (
<section class="animate space-y-4">
<div class="font-semibold text-black dark:text-white">{year}</div>
<div>
<ul class="flex flex-col gap-4">
{posts[year].map((post) => (
<li>
<ArrowCard entry={post} />
</li>
))}
</ul>
</div>
</section>
))
}
</div>
</div>
</Container>
</PageLayout>

72
src/styles/global.css Normal file
View File

@ -0,0 +1,72 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
html {
overflow-y: scroll;
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
html,
body {
@apply size-full;
}
body {
@apply font-sans antialiased;
@apply flex flex-col;
@apply bg-stone-100 dark:bg-stone-900;
@apply text-black/50 dark:text-white/75;
}
header {
@apply fixed top-0 left-0 right-0 z-50 py-5;
@apply bg-stone-100/75 dark:bg-stone-900/25;
@apply backdrop-blur-sm saturate-200;
}
main {
@apply flex-1 py-32;
}
footer {
@apply py-5 text-sm;
}
article {
@apply max-w-full prose dark:prose-invert prose-img:mx-auto prose-img:my-auto;
@apply prose-headings:font-semibold prose-p:font-serif;
@apply prose-headings:text-black prose-headings:dark:text-white;
}
@layer utilities {
article a {
@apply font-sans text-current underline underline-offset-2;
@apply decoration-black/15 dark:decoration-white/30;
@apply transition-colors duration-300 ease-in-out;
}
article a:hover {
@apply text-black dark:text-white;
@apply decoration-black/25 dark:decoration-white/50;
}
}
.animate {
@apply opacity-0 translate-y-3;
@apply transition-all duration-700 ease-out;
}
.animate.show {
@apply opacity-100 translate-y-0;
}
html #back-to-top {
@apply opacity-0 pointer-events-none;
}
html.scrolled #back-to-top {
@apply opacity-100 pointer-events-auto;
}

17
src/types.ts Normal file
View File

@ -0,0 +1,17 @@
export type Site = {
NAME: string;
EMAIL: string;
NUM_POSTS_ON_HOMEPAGE: number;
NUM_WORKS_ON_HOMEPAGE: number;
NUM_PROJECTS_ON_HOMEPAGE: number;
};
export type Metadata = {
TITLE: string;
DESCRIPTION: string;
};
export type Socials = {
NAME: string;
HREF: string;
}[];

20
tailwind.config.mjs Normal file
View File

@ -0,0 +1,20 @@
import defaultTheme from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
],
theme: {
extend: {
fontFamily: {
sans: ["IBM Plex Sans", ...defaultTheme.fontFamily.sans],
serif: ["Martel", ...defaultTheme.fontFamily.serif],
},
},
},
plugins: [require("@tailwindcss/typography"),
require("@tailwindcss/postcss")
],
};

View File

@ -1,5 +1,12 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"@*": ["./src/*"]
}
}
}