diff --git a/content/projects/gym-progress-app.mdx b/content/projects/gym-progress-app.mdx index b28dca2..168836d 100644 --- a/content/projects/gym-progress-app.mdx +++ b/content/projects/gym-progress-app.mdx @@ -3,6 +3,9 @@ title: 'gym-progress' description: 'A no-fluff progress tracker for gym rats — sets, reps, plate maths, weekly trend.' date: '2025-12-09' tags: ['typescript', 'react', 'pwa'] +featured: false +category: Web +role: Solo project --- The frustration: every other tracker wants me to log mood, sleep, water intake, diff --git a/content/projects/kmp-weather-app.mdx b/content/projects/kmp-weather-app.mdx index 7f1e819..673e0cf 100644 --- a/content/projects/kmp-weather-app.mdx +++ b/content/projects/kmp-weather-app.mdx @@ -3,6 +3,9 @@ title: 'KMP Weather' description: 'A small Kotlin Multiplatform weather app — Room, Koin, Ktor — sharing 90% of the code between Android and iOS.' date: '2026-02-18' tags: ['kotlin', 'kmp', 'android', 'ios', 'compose'] +featured: true +category: Mobile +role: Solo project --- A deliberate small project to learn KMP without hand-waving the hard parts: diff --git a/content/projects/link-safety-checker.mdx b/content/projects/link-safety-checker.mdx index 01d443e..7027894 100644 --- a/content/projects/link-safety-checker.mdx +++ b/content/projects/link-safety-checker.mdx @@ -3,6 +3,9 @@ title: 'Link Safety Checker' description: 'A URL safety analyzer that flags suspicious links via pattern matching, redirect tracing, and structural heuristics.' date: '2026-01-22' tags: ['python', 'security', 'url-analysis', 'cli'] +featured: false +category: Tools +role: Solo project --- A small Python tool that takes a URL and returns a risk score plus a list of diff --git a/content/projects/system-health-dashboard.mdx b/content/projects/system-health-dashboard.mdx index 9b52c82..26d95d3 100644 --- a/content/projects/system-health-dashboard.mdx +++ b/content/projects/system-health-dashboard.mdx @@ -4,6 +4,8 @@ description: 'Real-time desktop monitor for CPU, memory, disk, and network with date: '2026-04-12' tags: ['c#', 'wpf', '.net', 'observability'] featured: true +category: Desktop +role: Solo project --- A native Windows desktop application that streams live OS metrics into a single diff --git a/content/projects/tutoria-mobile-app.mdx b/content/projects/tutoria-mobile-app.mdx index a8d8fcd..5d0c0da 100644 --- a/content/projects/tutoria-mobile-app.mdx +++ b/content/projects/tutoria-mobile-app.mdx @@ -4,6 +4,8 @@ description: 'A React Native app that pairs printed manipulatives with a guided date: '2026-03-04' tags: ['react-native', 'typescript', 'a11y', 'mobile'] featured: true +category: Mobile +role: Bachelor project --- Tutoria treats reading practice as a physical-digital loop: a child arranges diff --git a/next.config.mjs b/next.config.mjs index 22338ed..f305d05 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,5 @@ import createMDX from '@next/mdx'; +import remarkFrontmatter from 'remark-frontmatter'; import remarkGfm from 'remark-gfm'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; @@ -10,13 +11,14 @@ const nextConfig = { images: { unoptimized: true }, pageExtensions: ['ts', 'tsx', 'mdx'], reactStrictMode: true, - transpilePackages: ['three'], }; const withMDX = createMDX({ extension: /\.mdx?$/, options: { - remarkPlugins: [remarkGfm], + // remarkFrontmatter strips the gray-matter `---` block so it is not + // rendered as body content (the page reads frontmatter via gray-matter). + remarkPlugins: [remarkFrontmatter, remarkGfm], rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]], }, }); diff --git a/package-lock.json b/package-lock.json index 3904292..6b35fe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,34 +8,28 @@ "name": "altanesmer-portfolio", "version": "0.1.0", "dependencies": { - "@fontsource/inter": "^5.2.8", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.5", + "@fontsource/ibm-plex-sans": "^5.2.6", + "@fontsource/newsreader": "^5.2.5", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.2.5", - "@react-three/drei": "^9.122.0", - "@react-three/fiber": "^8.18.0", "@types/mdx": "^2.0.13", "clsx": "^2.1.1", - "framer-motion": "^11.3.0", "gray-matter": "^4.0.3", - "lenis": "^1.3.23", - "lucide-react": "^1.14.0", "next": "^14.2.5", "react": "^18.3.1", "react-dom": "^18.3.1", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", + "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", - "tailwind-merge": "^2.5.0", - "three": "^0.184.0" + "tailwind-merge": "^2.5.0" }, "devDependencies": { "@types/node": "^20.14.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", - "@types/three": "^0.184.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", @@ -62,21 +56,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "license": "Apache-2.0" - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -174,28 +153,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fontsource/inter": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", - "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "node_modules/@fontsource/ibm-plex-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz", + "integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@fontsource/jetbrains-mono": { + "node_modules/@fontsource/ibm-plex-sans": { "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", - "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.2.8.tgz", + "integrity": "sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@fontsource/space-grotesk": { + "node_modules/@fontsource/newsreader": { "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@fontsource/space-grotesk/-/space-grotesk-5.2.10.tgz", - "integrity": "sha512-XNXEbT74OIITPqw2H6HXwPDp85fy43uxfBwFR5PU+9sLnjuLj12KlhVM9nZVN6q6dlKjkuN8JisW/OBxwxgUew==", + "resolved": "https://registry.npmjs.org/@fontsource/newsreader/-/newsreader-5.2.10.tgz", + "integrity": "sha512-TFaYzoFhDqarUyV2yYjgZZEwT4bpaj6sGBnXSnFknQ/QB8/9LzfY6IO9+inHOX4zzPp87Z7/KuG1OI5gr91Q3A==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -401,24 +380,6 @@ "react": ">=16" } }, - "node_modules/@mediapipe/tasks-vision": { - "version": "0.10.17", - "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", - "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", - "license": "Apache-2.0" - }, - "node_modules/@monogrid/gainmap-js": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", - "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", - "license": "MIT", - "dependencies": { - "promise-worker-transferable": "^1.0.4" - }, - "peerDependencies": { - "three": ">= 0.159.0" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -672,195 +633,6 @@ "node": ">=14" } }, - "node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", - "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/three": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", - "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "three": ">=0.126" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT" - }, - "node_modules/@react-three/drei": { - "version": "9.122.0", - "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", - "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mediapipe/tasks-vision": "0.10.17", - "@monogrid/gainmap-js": "^3.0.6", - "@react-spring/three": "~9.7.5", - "@use-gesture/react": "^10.3.1", - "camera-controls": "^2.9.0", - "cross-env": "^7.0.3", - "detect-gpu": "^5.0.56", - "glsl-noise": "^0.0.0", - "hls.js": "^1.5.17", - "maath": "^0.10.8", - "meshline": "^3.3.1", - "react-composer": "^5.0.3", - "stats-gl": "^2.2.8", - "stats.js": "^0.17.0", - "suspend-react": "^0.1.3", - "three-mesh-bvh": "^0.7.8", - "three-stdlib": "^2.35.6", - "troika-three-text": "^0.52.0", - "tunnel-rat": "^0.1.2", - "utility-types": "^3.11.0", - "zustand": "^5.0.1" - }, - "peerDependencies": { - "@react-three/fiber": "^8", - "react": "^18", - "react-dom": "^18", - "three": ">=0.137" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/@react-three/fiber": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", - "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.8", - "@types/react-reconciler": "^0.26.7", - "@types/webxr": "*", - "base64-js": "^1.5.1", - "buffer": "^6.0.3", - "its-fine": "^1.0.6", - "react-reconciler": "^0.27.0", - "react-use-measure": "^2.1.7", - "scheduler": "^0.21.0", - "suspend-react": "^0.1.3", - "zustand": "^3.7.1" - }, - "peerDependencies": { - "expo": ">=43.0", - "expo-asset": ">=8.4", - "expo-file-system": ">=11.0", - "expo-gl": ">=11.0", - "react": ">=18 <19", - "react-dom": ">=18 <19", - "react-native": ">=0.64", - "three": ">=0.133" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "expo-asset": { - "optional": true - }, - "expo-file-system": { - "optional": true - }, - "expo-gl": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@react-three/fiber/node_modules/scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/@react-three/fiber/node_modules/zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", - "license": "MIT", - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -891,12 +663,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -917,12 +683,6 @@ "@types/ms": "*" } }, - "node_modules/@types/draco3d": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", - "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -985,12 +745,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1017,47 +771,12 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-reconciler": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", - "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.184.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", - "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", @@ -1615,24 +1334,6 @@ "win32" ] }, - "node_modules/@use-gesture/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", - "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", - "license": "MIT" - }, - "node_modules/@use-gesture/react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", - "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", - "license": "MIT", - "dependencies": { - "@use-gesture/core": "10.3.1" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2018,26 +1719,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.10.27", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", @@ -2051,15 +1732,6 @@ "node": ">=6.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2131,30 +1803,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2236,15 +1884,6 @@ "node": ">= 6" } }, - "node_modules/camera-controls": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", - "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.126.1" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001792", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", @@ -2442,28 +2081,11 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2636,15 +2258,6 @@ "node": ">=6" } }, - "node_modules/detect-gpu": { - "version": "5.0.70", - "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", - "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", - "license": "MIT", - "dependencies": { - "webgl-constants": "^1.1.1" - } - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -2685,12 +2298,6 @@ "node": ">=6.0.0" } }, - "node_modules/draco3d": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", - "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", - "license": "Apache-2.0" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3572,11 +3179,18 @@ "reusify": "^1.0.4" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -3676,6 +3290,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -3690,33 +3312,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/framer-motion": { - "version": "11.18.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", - "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", - "license": "MIT", - "dependencies": { - "motion-dom": "^11.18.1", - "motion-utils": "^11.18.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3962,12 +3557,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glsl-noise": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", - "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4232,32 +3821,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hls.js": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", - "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", - "license": "Apache-2.0" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4268,12 +3831,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4696,12 +4253,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4858,6 +4409,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4878,27 +4430,6 @@ "node": ">= 0.4" } }, - "node_modules/its-fine": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", - "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.28.0" - }, - "peerDependencies": { - "react": ">=18.0" - } - }, - "node_modules/its-fine/node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -5036,37 +4567,6 @@ "node": ">=0.10" } }, - "node_modules/lenis": { - "version": "1.3.23", - "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", - "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", - "license": "MIT", - "workspaces": [ - "packages/*", - "playground", - "playground/*" - ], - "funding": { - "type": "github", - "url": "https://github.com/sponsors/darkroomengineering" - }, - "peerDependencies": { - "@nuxt/kit": ">=3.0.0", - "react": ">=17.0.0", - "vue": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - }, - "react": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5081,15 +4581,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5162,25 +4653,6 @@ "dev": true, "license": "ISC" }, - "node_modules/lucide-react": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", - "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/maath": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", - "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", - "license": "MIT", - "peerDependencies": { - "@types/three": ">=0.134.0", - "three": ">=0.134.0" - } - }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", @@ -5265,6 +4737,36 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-gfm": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", @@ -5522,21 +5024,6 @@ "node": ">= 8" } }, - "node_modules/meshline": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", - "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "license": "MIT" - }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -5606,6 +5093,22 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", @@ -6301,21 +5804,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/motion-dom": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", - "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^11.18.1" - } - }, - "node_modules/motion-utils": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", - "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6503,6 +5991,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6771,6 +6260,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7034,12 +6524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7153,20 +6637,11 @@ } } }, - "node_modules/promise-worker-transferable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", - "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", - "license": "Apache-2.0", - "dependencies": { - "is-promise": "^2.1.0", - "lie": "^3.0.2" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -7227,18 +6702,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-composer": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", - "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -7256,48 +6719,9 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, - "node_modules/react-reconciler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", - "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.21.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.0.0" - } - }, - "node_modules/react-reconciler/node_modules/scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7482,6 +6906,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -7562,15 +7002,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -7832,6 +7263,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7844,6 +7276,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7979,32 +7412,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stats-gl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", - "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", - "license": "MIT", - "dependencies": { - "@types/three": "*", - "three": "^0.170.0" - }, - "peerDependencies": { - "@types/three": "*", - "three": "*" - } - }, - "node_modules/stats-gl/node_modules/three": { - "version": "0.170.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", - "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", - "license": "MIT" - }, - "node_modules/stats.js": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", - "license": "MIT" - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8373,15 +7780,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "license": "MIT", - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/tailwind-merge": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", @@ -8482,45 +7880,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.184.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", - "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", - "license": "MIT" - }, - "node_modules/three-mesh-bvh": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", - "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", - "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", - "license": "MIT", - "peerDependencies": { - "three": ">= 0.151.0" - } - }, - "node_modules/three-stdlib": { - "version": "2.36.1", - "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", - "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", - "license": "MIT", - "dependencies": { - "@types/draco3d": "^1.4.0", - "@types/offscreencanvas": "^2019.6.4", - "@types/webxr": "^0.5.2", - "draco3d": "^1.4.1", - "fflate": "^0.6.9", - "potpack": "^1.0.1" - }, - "peerDependencies": { - "three": ">=0.128.0" - } - }, - "node_modules/three-stdlib/node_modules/fflate": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -8592,36 +7951,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/troika-three-text": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", - "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.52.4", - "troika-worker-utils": "^0.52.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-three-utils": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", - "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-worker-utils": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", - "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", - "license": "MIT" - }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -8671,43 +8000,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tunnel-rat": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", - "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", - "license": "MIT", - "dependencies": { - "zustand": "^4.3.2" - } - }, - "node_modules/tunnel-rat/node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9028,15 +8320,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9044,15 +8327,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -9081,21 +8355,11 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/webgl-constants": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", - "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" - }, - "node_modules/webgl-sdf-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", - "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9327,35 +8591,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zustand": { - "version": "5.0.13", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", - "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e50098d..ddd16ab 100644 --- a/package.json +++ b/package.json @@ -13,34 +13,28 @@ "format": "prettier --write ." }, "dependencies": { - "@fontsource/inter": "^5.2.8", - "@fontsource/jetbrains-mono": "^5.2.8", - "@fontsource/space-grotesk": "^5.2.10", + "@fontsource/ibm-plex-mono": "^5.2.5", + "@fontsource/ibm-plex-sans": "^5.2.6", + "@fontsource/newsreader": "^5.2.5", "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.2.5", - "@react-three/drei": "^9.122.0", - "@react-three/fiber": "^8.18.0", "@types/mdx": "^2.0.13", "clsx": "^2.1.1", - "framer-motion": "^11.3.0", "gray-matter": "^4.0.3", - "lenis": "^1.3.23", - "lucide-react": "^1.14.0", "next": "^14.2.5", "react": "^18.3.1", "react-dom": "^18.3.1", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0", + "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", - "tailwind-merge": "^2.5.0", - "three": "^0.184.0" + "tailwind-merge": "^2.5.0" }, "devDependencies": { "@types/node": "^20.14.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", - "@types/three": "^0.184.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 53bc92c..3ae8b23 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,42 +1,242 @@ -import MotionSection from '@/components/MotionSection'; +import Reveal from '@/components/ui/Reveal'; +import Button from '@/components/ui/Button'; +import Kicker from '@/components/ui/Kicker'; export const metadata = { title: 'About — Altan Esmer', - description: 'A little about who I am and what I work on.', + description: + 'Full-stack software engineer based in Denmark, building reliable, accessible software across web, mobile, and desktop.', }; +const TIMELINE = [ + { + year: '2025', + title: 'Building & writing', + desc: 'Shipping gym-progress and publishing project case studies and notes.', + }, + { + year: '2024', + title: 'Desktop & cross-platform', + desc: 'Built the System Health Dashboard (C# / WPF) and KMP Weather with Kotlin Multiplatform.', + }, + { + year: '2023', + title: 'B.Sc. Computer Science', + desc: 'Graduated from the University of Southern Denmark. Bachelor project: Tutoria, a phonics app for children with dyslexia.', + }, + { + year: '2019', + title: 'Started university', + desc: 'Began the B.Sc. in Computer Science at the University of Southern Denmark.', + }, +]; + export default function AboutPage() { return ( -
- -

- About -

-
-

- I am a software engineer with a focus on frontend architecture and web - performance. Most of my work lives at the intersection of product and - platform — I care about the experience end users have just as much as - the developer experience of the teams building it. -

-

- I have spent time working on real-time systems, design systems, and - static site infrastructure. I enjoy the kind of problem that requires - you to understand both the constraints of the browser and the - constraints of the server — and to make thoughtful tradeoffs between - them. When something is slow, I want to know exactly why, and I tend - to reach for profiling tools and architectural changes before adding - more infrastructure. -

-

- Outside of engineering work, I read widely — mostly history, - philosophy of mind, and the occasional novel. I believe the best - technical decisions are made by people who can think clearly beyond - the technical domain, and I try to cultivate that in myself. If any - of my writing resonates with you, feel free to reach out. -

-
-
-
+
+ {/* Intro block */} + +
+ {/* Left column */} +
+ About +

+ Hi, I’m Altan. +

+ +

+ I’m a full-stack software engineer based in Denmark, building reliable, + accessible software across web, mobile, and desktop. +

+ +

+ I work mainly in TypeScript, React, and Node, and I’m comfortable reaching for + the right tool when a problem calls for it — Kotlin Multiplatform for shared mobile + code, C# and .NET on the desktop, or Python for quick, focused utilities. +

+ +

+ What ties it together is a preference for calm, careful work: small testable pieces, + clear interfaces, strong accessibility, and performance treated as a feature. I’m + currently open to remote roles and freelance work across the EU. +

+ +
+ + +
+
+ + {/* Right column — portrait placeholder */} +
+
+ AE +
+ + Portrait + +
+
+
+ + {/* Timeline block */} + +
+

+ Education & milestones +

+ +
+ {TIMELINE.map((item) => ( +
+ +
+ {item.year} +
+
+ {item.title} +
+
+ {item.desc} +
+
+ ))} +
+
+
+
); } diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx index 79e33ea..1e917c3 100644 --- a/src/app/blog/[slug]/page.tsx +++ b/src/app/blog/[slug]/page.tsx @@ -1,15 +1,14 @@ import { notFound } from 'next/navigation'; -import { getAllSlugs, getContentBySlug } from '@/lib/content'; +import Link from 'next/link'; +import Reveal from '@/components/ui/Reveal'; +import ArticleNav from '@/components/ui/ArticleNav'; +import { getAllSlugs, getContentBySlug, getAdjacentContent } from '@/lib/content'; export function generateStaticParams() { return getAllSlugs('posts').map((slug) => ({ slug })); } -export async function generateMetadata({ - params, -}: { - params: { slug: string }; -}) { +export async function generateMetadata({ params }: { params: { slug: string } }) { const item = getContentBySlug('posts', params.slug); if (!item) return {}; return { @@ -18,47 +17,116 @@ export async function generateMetadata({ }; } -export default async function BlogPostPage({ - params, -}: { - params: { slug: string }; -}) { +function formatMonthYear(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }); +} + +function estimateReadTime(body: string, readingTime?: string): string { + if (readingTime) return readingTime; + return `${Math.max(1, Math.round(body.trim().split(/\s+/).length / 200))} min read`; +} + +export default async function BlogPostPage({ params }: { params: { slug: string } }) { const item = getContentBySlug('posts', params.slug); if (!item) notFound(); + const { prev, next } = getAdjacentContent('posts', params.slug); + const { default: MDXContent } = await import( `../../../../content/posts/${params.slug}.mdx` ); + const fm = item.frontmatter; + const tag = fm.tags && fm.tags.length > 0 ? fm.tags[0] : null; + const readTime = estimateReadTime(item.body, fm.readingTime); + const date = formatMonthYear(fm.date); + return ( -
-
-
-

- {item.frontmatter.title} -

-

- {new Date(item.frontmatter.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} -

- {item.frontmatter.tags && item.frontmatter.tags.length > 0 && ( -
    - {item.frontmatter.tags.map((tag) => ( -
  • + {/* Back link */} + + ← All posts + + + + {/* Meta row */} +
    + {date} + · + {readTime} + {tag && ( + <> + · + {tag} -
  • - ))} -
- )} -
- + + + )} +
+ + {/* Title */} +

+ {fm.title} +

+ + {/* Description */} +

+ {fm.description} +

+ + + {/* Body */} + +
+ +
+
+ + {/* Prev / Next navigation */} + - ); } diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index 033c8e9..06ff157 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,52 +1,146 @@ import Link from 'next/link'; -import MotionSection from '@/components/MotionSection'; +import Reveal from '@/components/ui/Reveal'; +import Kicker from '@/components/ui/Kicker'; import { getAllContent } from '@/lib/content'; export const metadata = { - title: 'Blog — Altan Esmer', - description: 'Writing on software engineering, web performance, and things I am building.', + title: 'Writing — Altan Esmer', + description: + "Occasional notes on engineering, performance, accessibility, and whatever I'm building.", }; +function formatMonthYear(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }); +} + +function estimateReadTime(body: string, readingTime?: string): string { + if (readingTime) return readingTime; + return `${Math.max(1, Math.round(body.trim().split(/\s+/).length / 200))} min read`; +} + export default function BlogPage() { const posts = getAllContent('posts'); return ( -
- -

- Blog -

- {posts.length > 0 ? ( -
    - {posts.map((post) => ( -
  • +
    + + Writing +

    + Blog +

    +

    + Occasional notes on engineering, performance, accessibility, and whatever I'm + building. +

    +
    + + +
    + {posts.map((p) => { + const fm = p.frontmatter; + const tag = fm.tags && fm.tags.length > 0 ? fm.tags[0] : null; + const readTime = estimateReadTime(p.body, fm.readingTime); + const date = formatMonthYear(fm.date); + + return ( -

    - {post.frontmatter.title} + {/* Meta row */} +
    + {date} + · + {readTime} + {tag && ( + <> + · + + {tag} + + + )} +
    + + {/* Title */} +

    + {fm.title}

    -

    - {post.frontmatter.description} -

    -

    - {new Date(post.frontmatter.date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} + + {/* Description */} +

    + {fm.description}

    -
  • - ))} -
- ) : ( -

- No posts yet. Check back soon. -

- )} -
-
+ ); + })} + + + ); } diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx new file mode 100644 index 0000000..842ceaa --- /dev/null +++ b/src/app/contact/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import ContactForm from '@/components/ContactForm'; + +export const metadata: Metadata = { + title: 'Contact — Altan Esmer', + description: + 'Tell me about your project or role. I read every message and reply within 24 hours.', +}; + +export default function ContactPage() { + return ; +} diff --git a/src/app/globals.css b/src/app/globals.css index ba9f032..8f4f64e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,69 +1,67 @@ -@import '@fontsource/space-grotesk/300.css'; -@import '@fontsource/space-grotesk/400.css'; -@import '@fontsource/space-grotesk/500.css'; -@import '@fontsource/space-grotesk/600.css'; -@import '@fontsource/space-grotesk/700.css'; -@import '@fontsource/jetbrains-mono/400.css'; -@import '@fontsource/jetbrains-mono/500.css'; -@import '@fontsource/jetbrains-mono/600.css'; -@import '@fontsource/jetbrains-mono/700.css'; -@import '@fontsource/inter/400.css'; -@import '@fontsource/inter/500.css'; -@import '@fontsource/inter/600.css'; +@import '@fontsource/newsreader/400.css'; +@import '@fontsource/newsreader/500.css'; +@import '@fontsource/newsreader/600.css'; +@import '@fontsource/newsreader/400-italic.css'; +@import '@fontsource/newsreader/500-italic.css'; +@import '@fontsource/ibm-plex-sans/400.css'; +@import '@fontsource/ibm-plex-sans/500.css'; +@import '@fontsource/ibm-plex-sans/600.css'; +@import '@fontsource/ibm-plex-sans/700.css'; +@import '@fontsource/ibm-plex-mono/400.css'; +@import '@fontsource/ibm-plex-mono/500.css'; @tailwind base; @tailwind components; @tailwind utilities; /* ------------------------------------------------------------------ */ -/* Design tokens — AlesSystems */ +/* Design tokens — light editorial */ /* ------------------------------------------------------------------ */ :root { /* Surfaces */ - --bg: #0A0E27; - --bg-2: #0d1230; - --surface: #1A1B3A; - --surface-2: #232450; - --line: #2a2c5a; + --bg: #faf8f4; + --surface: #ffffff; + --placeholder: #f3efe7; /* Text */ - --text: #E8EAF6; - --muted: #9aa0d4; - --dim: #6e75a8; - - /* Brand accents */ - --blue: #3B82F6; - --violet: #8B5CF6; - --teal: #14B8A6; - --magenta: #EC4899; - --green: #10B981; - --amber: #F59E0B; - --red: #EF4444; - - /* Signature gradients */ - --grad: linear-gradient(120deg, #3B82F6 0%, #8B5CF6 50%, #14B8A6 100%); - --grad-soft: linear-gradient( - 120deg, - rgba(59, 130, 246, 0.18), - rgba(139, 92, 246, 0.18), - rgba(20, 184, 166, 0.18) - ); - --grad-iri: linear-gradient(120deg, #3B82F6, #8B5CF6, #EC4899, #14B8A6, #3B82F6); - - /* Legacy aliases (still referenced in a few places — keep working) */ - --color-bg: #0A0E27; - --color-surface: #1A1B3A; - --color-accent-blue: #3B82F6; - --color-accent-purple: #8B5CF6; - --color-accent-teal: #14B8A6; - --color-highlight: #EC4899; - --color-text: #E8EAF6; -} - -@property --ang { - syntax: ''; - initial-value: 0deg; - inherits: false; + --ink: #1c1a17; + --prose: #3f3b35; + --secondary: #57534e; + --muted: #8a8377; + --faint: #a8a195; + + /* Borders & dividers */ + --border: #e9e4da; + --border-strong: #ddd6c9; + --border-light: #ece7dd; + --divider: #f0ece3; + --divider-2: #f6f3ec; + --border-hover: #d7d0c2; + + /* Accents */ + --blue: #2563eb; + --blue-hover: #1d4ed8; + --amber: #d97706; + --amber-code: #b45309; + --green: #16a34a; + --red: #dc2626; + --focus-ring: rgba(37, 99, 235, 0.14); + + /* Tints */ + --tag-bg: #f6f3ec; + --code-bg: #f5f2ea; + --code-ink: #2b2823; + --code-inline-bg: #f0ece3; + + /* Type families */ + --font-serif: 'Newsreader', Georgia, serif; + --font-sans: 'IBM Plex Sans', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', ui-monospace, monospace; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(28, 26, 23, 0.04); + --shadow-card-hover: 0 20px 40px -24px rgba(28, 26, 23, 0.3); + --shadow-hero: 0 1px 2px rgba(28, 26, 23, 0.04), 0 16px 36px -22px rgba(28, 26, 23, 0.22); } /* ------------------------------------------------------------------ */ @@ -81,431 +79,387 @@ html, body { margin: 0; padding: 0; - background: var(--bg); - color: var(--text); - font-family: 'Inter', system-ui, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } body { - background: - radial-gradient(1200px 700px at 80% -10%, rgba(139, 92, 246, 0.18), transparent 60%), - radial-gradient(900px 600px at -10% 30%, rgba(59, 130, 246, 0.14), transparent 60%), - radial-gradient(900px 600px at 110% 110%, rgba(20, 184, 166, 0.12), transparent 60%), - var(--bg); - overflow-x: hidden; -} - -/* SVG-noise overlay */ -body::before { - content: ''; - position: fixed; - inset: 0; - background-image: url("data:image/svg+xml;utf8,"); - pointer-events: none; - opacity: 0.35; - z-index: 1; - mix-blend-mode: overlay; + background: var(--bg); + color: var(--ink); + font-family: var(--font-sans); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Selection — magenta-tinted */ ::selection { - background: rgba(236, 72, 153, 0.4); - color: #fff; + background: #cfe0ff; } -/* Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} -::-webkit-scrollbar-track { - background: var(--bg); -} -::-webkit-scrollbar-thumb { - background: var(--surface); - border-radius: 3px; -} -::-webkit-scrollbar-thumb:hover { - background: var(--violet); +input, +textarea, +select, +button { + font-family: inherit; } /* ------------------------------------------------------------------ */ -/* Type families */ +/* Type helpers */ /* ------------------------------------------------------------------ */ -.mono { - font-family: 'JetBrains Mono', ui-monospace, monospace; +.font-serif { + font-family: var(--font-serif); } -.display { - font-family: 'Space Grotesk', system-ui, sans-serif; - letter-spacing: -0.03em; +.font-mono { + font-family: var(--font-mono); +} + +/* Section kicker — small uppercase amber label */ +.kicker { + font-size: 12.5px; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--amber); } /* ------------------------------------------------------------------ */ -/* Custom cursor */ +/* Layout */ /* ------------------------------------------------------------------ */ -@media (min-width: 801px) { - body.has-custom-cursor { - cursor: none; - } - body.has-custom-cursor a, - body.has-custom-cursor button, - body.has-custom-cursor input, - body.has-custom-cursor textarea, - body.has-custom-cursor [role='button'] { - cursor: none; +.wrap { + max-width: 1120px; + margin: 0 auto; + padding: 0 28px; +} +@media (max-width: 600px) { + .wrap { + padding: 0 20px; } } -.cursor-dot, -.cursor-ring { - position: fixed; - top: 0; - left: 0; - pointer-events: none; - z-index: 10000; - border-radius: 999px; - transform: translate(-50%, -50%); - will-change: transform, width, height, background; -} -.cursor-dot { - width: 6px; - height: 6px; - background: var(--magenta); -} -.cursor-ring { - width: 32px; - height: 32px; - border: 1px solid rgba(232, 234, 246, 0.5); +/* ------------------------------------------------------------------ */ +/* Reveal on scroll */ +/* ------------------------------------------------------------------ */ +.reveal { + opacity: 0; + transform: translateY(18px); transition: - width 0.18s, - height 0.18s, - background 0.18s, - border-color 0.18s; -} -.cursor-ring.hot { - width: 56px; - height: 56px; - border-color: var(--magenta); - background: rgba(236, 72, 153, 0.08); -} -@media (max-width: 800px) { - .cursor-dot, - .cursor-ring { - display: none; - } + opacity 0.7s cubic-bezier(0.22, 0.61, 0.36, 1), + transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1); +} +.reveal.in { + opacity: 1; + transform: none; } /* ------------------------------------------------------------------ */ -/* Keyframes */ +/* MDX prose — used by case studies and blog posts */ /* ------------------------------------------------------------------ */ -@keyframes iri { - 0% { - background-position: 0% 50%; - } - 100% { - background-position: 300% 50%; - } +.prose-mdx { + max-width: none; + color: var(--prose); + font-size: 17px; + line-height: 1.8; } -@keyframes blink { - 50% { - opacity: 0; - } +.prose-mdx > :first-child { + margin-top: 0; } -@keyframes pulse { - 50% { - opacity: 0.5; - } +.prose-mdx h2 { + font-family: var(--font-serif); + font-weight: 600; + font-size: 27px; + letter-spacing: -0.01em; + color: var(--ink); + margin: 34px 0 0; } -@keyframes chipIn { - to { - opacity: 1; - transform: translateY(0); - } +.prose-mdx h3 { + font-family: var(--font-serif); + font-weight: 600; + font-size: 21px; + letter-spacing: -0.01em; + color: var(--ink); + margin: 28px 0 0; } -@keyframes spinAng { - to { - --ang: 360deg; - } +.prose-mdx p { + margin: 14px 0 0; } -@keyframes toastIn { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } +.prose-mdx a { + color: var(--blue); + text-decoration: underline; + text-underline-offset: 3px; +} +.prose-mdx a:hover { + text-decoration: none; +} +.prose-mdx strong { + font-weight: 600; + color: var(--ink); +} +.prose-mdx ul, +.prose-mdx ol { + margin: 14px 0 0; + padding-left: 24px; +} +.prose-mdx li { + margin-top: 7px; +} +.prose-mdx blockquote { + margin: 22px 0 0; + padding: 4px 0 4px 20px; + border-left: 3px solid var(--amber); + color: var(--secondary); + font-style: italic; +} +.prose-mdx img { + max-width: 100%; + height: auto; + border-radius: 12px; + border: 1px solid var(--border); + margin-top: 24px; +} +.prose-mdx hr { + border: none; + border-top: 1px solid var(--border-light); + margin: 36px 0; +} +/* Inline code */ +.prose-mdx code { + font-family: var(--font-mono); + font-size: 0.88em; + background: var(--code-inline-bg); + border: 1px solid var(--border); + padding: 1px 6px; + border-radius: 5px; + color: var(--amber-code); +} +/* Code blocks */ +.prose-mdx pre { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 18px 20px; + overflow: auto; + margin: 24px 0; +} +.prose-mdx pre code { + font-family: var(--font-mono); + font-size: 13.5px; + line-height: 1.7; + color: var(--code-ink); + background: transparent; + border: none; + padding: 0; +} +.prose-mdx h2 + p, +.prose-mdx h3 + p { + margin-top: 14px; } /* ------------------------------------------------------------------ */ -/* Reusable atoms */ +/* Buttons */ /* ------------------------------------------------------------------ */ - -/* Iridescent border — apply to a positioned (relative/absolute) parent. - Use directly via class, OR via the helper above. */ -.iri-border { - position: relative; +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 600; + padding: 13px 22px; + border-radius: 10px; + text-decoration: none; + cursor: pointer; + border: 1px solid transparent; + line-height: 1; + transition: + background 0.2s ease, + transform 0.2s ease, + border-color 0.2s ease, + color 0.2s ease; } -.iri-border::before { - content: ''; - position: absolute; - inset: 0; - border-radius: inherit; - padding: 1px; - background: var(--grad-iri); - background-size: 300% 300%; - -webkit-mask: - linear-gradient(#000 0 0) content-box, - linear-gradient(#000 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - opacity: 0.35; - animation: iri 10s linear infinite; - pointer-events: none; +.btn-sm { + font-size: 14.5px; + padding: 11px 18px; } - -/* Conic-gradient hover border for bento cards. Apply `.bento-card` to - a positioned card; opacity transitions in on :hover and rotates --ang. */ -.bento-card { - position: relative; +.btn-primary { + background: var(--blue); + color: #fff; + border-color: var(--blue); } -.bento-card::before { - content: ''; - position: absolute; - inset: -1px; - border-radius: inherit; - padding: 1px; - background: conic-gradient( - from var(--ang, 0deg), - transparent 0%, - var(--blue) 25%, - var(--violet) 50%, - var(--magenta) 65%, - var(--teal) 80%, - transparent 100% - ); - -webkit-mask: - linear-gradient(#000 0 0) content-box, - linear-gradient(#000 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - opacity: 0; - transition: opacity 0.3s; - pointer-events: none; +.btn-primary:hover { + background: var(--blue-hover); + border-color: var(--blue-hover); + transform: translateY(-1px); } -.bento-card:hover::before { - opacity: 1; - animation: spinAng 4s linear infinite; +.btn-secondary { + background: var(--surface); + color: var(--ink); + border-color: var(--border-strong); +} +.btn-secondary:hover { + border-color: var(--blue); + color: var(--blue); } -/* Card preview gradient backgrounds */ -.pv-grad-1 { - background: - radial-gradient(circle at 30% 30%, #3b82f6, transparent 60%), - radial-gradient(circle at 70% 70%, #8b5cf6, transparent 60%), - #14163b; -} -.pv-grad-2 { - background: - radial-gradient(circle at 70% 30%, #14b8a6, transparent 60%), - radial-gradient(circle at 30% 70%, #3b82f6, transparent 60%), - #0e1a30; -} -.pv-grad-3 { - background: - radial-gradient(circle at 50% 30%, #ec4899, transparent 60%), - radial-gradient(circle at 50% 80%, #8b5cf6, transparent 60%), - #1a1130; -} -.pv-grad-4 { - background: - radial-gradient(circle at 30% 30%, #f59e0b, transparent 50%), - radial-gradient(circle at 80% 80%, #ec4899, transparent 60%), - #1c1230; -} -.pv-grad-5 { - background: linear-gradient(135deg, #0e1a30, #14163b 50%, #2a1a3a); -} -.pv-grad-6 { - background: - radial-gradient(circle at 60% 40%, #14b8a6, transparent 60%), - #0e1a30; -} -.pv-grad-7 { - background: - radial-gradient(circle at 30% 70%, #8b5cf6, transparent 50%), - #14163b; -} -.pv-grid { - position: absolute; - inset: 0; - background-image: - linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); - background-size: 24px 24px; - -webkit-mask-image: radial-gradient(circle at 50% 50%, #000 30%, transparent 75%); - mask-image: radial-gradient(circle at 50% 50%, #000 30%, transparent 75%); - pointer-events: none; +/* Inline blue link */ +.link-blue { + color: var(--blue); + font-weight: 600; + text-decoration: none; + cursor: pointer; +} +.link-blue:hover { + text-decoration: underline; } -/* Chips */ -.chip { - font-family: 'JetBrains Mono', ui-monospace, monospace; - font-size: 11px; - padding: 4px 9px; +/* ------------------------------------------------------------------ */ +/* Tag pill */ +/* ------------------------------------------------------------------ */ +.tag { + font-size: 12px; + color: var(--secondary); + background: var(--tag-bg); + border: 1px solid var(--border-light); + padding: 4px 10px; border-radius: 999px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--line); - color: var(--muted); display: inline-flex; align-items: center; - line-height: 1; -} -.chip.fe { - color: #93c5fd; - border-color: rgba(59, 130, 246, 0.3); - background: rgba(59, 130, 246, 0.08); -} -.chip.be { - color: #c4b5fd; - border-color: rgba(139, 92, 246, 0.3); - background: rgba(139, 92, 246, 0.08); -} -.chip.de { - color: #5eead4; - border-color: rgba(20, 184, 166, 0.3); - background: rgba(20, 184, 166, 0.08); -} -.chip.ds { - color: #fbcfe8; - border-color: rgba(236, 72, 153, 0.3); - background: rgba(236, 72, 153, 0.08); -} -.chip.in { - /* "in" animation chip — wraps with chipIn */ - opacity: 0; - transform: translateY(6px); - animation: chipIn 0.35s forwards; + line-height: 1.4; } -/* Iridescent text — apply to the word that should shimmer */ -.grad-text { - background: var(--grad-iri); - background-size: 300% 300%; - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: iri 8s linear infinite; +/* ------------------------------------------------------------------ */ +/* Cards */ +/* ------------------------------------------------------------------ */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: var(--shadow-sm); } - -/* Reveal on scroll */ -.reveal { - opacity: 0; - transform: translateY(24px); +.proj-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + padding: 24px; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 14px; + text-decoration: none; + color: var(--ink); + box-shadow: var(--shadow-sm); transition: - opacity 0.8s cubic-bezier(0.2, 0.7, 0.2, 1), - transform 0.8s cubic-bezier(0.2, 0.7, 0.2, 1); + transform 0.25s ease, + box-shadow 0.25s ease, + border-color 0.25s ease; } -.reveal.in { - opacity: 1; - transform: translateY(0); +.proj-card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-card-hover); + border-color: var(--border-hover); } -/* Layout wrap */ -.wrap { - max-width: 1400px; - margin: 0 auto; - padding: 0 32px; - position: relative; - z-index: 2; +/* ------------------------------------------------------------------ */ +/* Form fields */ +/* ------------------------------------------------------------------ */ +.field-label { + display: block; + font-size: 14px; + font-weight: 600; + margin-bottom: 7px; + color: var(--ink); +} +.field { + width: 100%; + padding: 11px 14px; + border-radius: 9px; + background: var(--surface); + font-size: 15px; + color: var(--ink); + border: 1px solid var(--border-strong); + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; } -@media (max-width: 640px) { - .wrap { - padding: 0 16px; - } +.field:focus { + outline: none; + border-color: var(--blue); + box-shadow: 0 0 0 3px var(--focus-ring); } - -/* Section header strip — `02 // name ` */ -.section-head { - display: flex; - align-items: baseline; - gap: 16px; - margin-bottom: 32px; - font-family: 'JetBrains Mono', ui-monospace, monospace; - font-size: 13px; - color: var(--muted); +.field-error { + border-color: var(--red); } -.section-head .num { - color: var(--magenta); +textarea.field { + resize: vertical; + line-height: 1.6; + font-family: var(--font-sans); } -.section-head .line { - flex: 1; - height: 1px; - background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), transparent); +select.field { + cursor: pointer; + appearance: auto; } - -/* ------------------------------------------------------------------ */ -/* Tailwind components — kept */ -/* ------------------------------------------------------------------ */ -@layer components { - .glass-card { - @apply bg-surface/60 backdrop-blur-md border border-white/10 rounded-2xl; - } - - .glow-border { - @apply border border-accent-blue/30 hover:border-accent-blue/70 transition-colors duration-300; - } - - .gradient-text { - @apply bg-gradient-to-r from-accent-blue via-accent-purple to-accent-teal bg-clip-text text-transparent; - } - - .section-padding { - @apply px-6 py-20 md:px-12 lg:px-20; - } - - .terminal-text { - @apply font-mono text-sm leading-relaxed; - } +.field-msg { + font-size: 13px; + color: var(--red); + margin-top: 6px; } /* ------------------------------------------------------------------ */ -/* MDX prose — used by /blog and /projects */ +/* Nav */ /* ------------------------------------------------------------------ */ -.prose-mdx { - @apply max-w-none; -} -.prose-mdx h2 { - @apply mt-10 text-2xl font-semibold text-port-text; -} -.prose-mdx h3 { - @apply mt-8 text-xl font-semibold text-port-text; -} -.prose-mdx p { - @apply mt-4 leading-relaxed text-port-text/80; +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: rgba(250, 248, 244, 0.82); + backdrop-filter: saturate(180%) blur(14px); + -webkit-backdrop-filter: saturate(180%) blur(14px); + border-bottom: 1px solid var(--border); } -.prose-mdx a { - @apply underline underline-offset-4 hover:no-underline text-accent-blue; +.nav-link { + position: relative; + padding: 8px 13px; + color: var(--secondary); + font-size: 15px; + font-weight: 500; + text-decoration: none; + border-radius: 8px; + transition: color 0.2s ease; +} +.nav-link:hover { + color: var(--blue); +} +.nav-link .nav-underline { + position: absolute; + left: 13px; + right: 13px; + bottom: 1px; + height: 2px; + background: var(--amber); + border-radius: 2px; } -.prose-mdx code { - @apply rounded bg-surface px-1.5 py-0.5 text-sm font-mono text-accent-teal; +.footer-link { + font-size: 14.5px; + color: var(--prose); + text-decoration: none; + cursor: pointer; + transition: color 0.2s ease; } -.prose-mdx pre { - @apply mt-4 overflow-x-auto rounded-lg bg-surface p-4 text-sm; +.footer-link:hover { + color: var(--blue); } -.prose-mdx pre code { - @apply bg-transparent p-0; + +/* Prev/next article links */ +.article-nav-link { + transition: color 0.2s ease; } -.prose-mdx ul { - @apply mt-4 list-disc pl-6; +.article-nav-link:hover { + color: var(--blue) !important; } /* ------------------------------------------------------------------ */ /* Reduced motion */ /* ------------------------------------------------------------------ */ @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } *, *::before, *::after { diff --git a/src/app/hire/page.tsx b/src/app/hire/page.tsx deleted file mode 100644 index 0c48272..0000000 --- a/src/app/hire/page.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import Link from 'next/link'; - -export const metadata = { - title: 'Hire me — Altan Esmer', - description: - 'Open to full-time, contract, and freelance software engineering work.', -}; - -const openTo = [ - 'Full-time roles', - 'Contract engagements', - 'Freelance projects', - 'Technical consulting', -]; - -export default function HirePage() { - return ( -
-
-
- 06 // - ./hire-me - -
- -

- Let's work -
- together. -

- -

- I'm a full-stack engineer focused on developer tooling and - performant web experiences. If you're building something - interesting, I'd love to hear about it. -

- -
-
- open to -
-
    - {openTo.map((item) => ( -
  • - - {item} -
  • - ))} -
- -
- - $ ./email-altan - - - or use the contact form - -
-
- - - ← back home - -
-
- ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e423f76..472c7de 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,25 +1,22 @@ import type { Metadata } from 'next'; import './globals.css'; import Nav from '@/components/Nav'; -import Cursor from '@/components/Cursor'; -import SmoothScrollProvider from '@/components/SmoothScrollProvider'; +import Footer from '@/components/Footer'; export const metadata: Metadata = { metadataBase: new URL('https://altanesmer.github.io'), - title: 'Altan Esmer — Portfolio', + title: 'Altan Esmer — Software Engineer', description: - 'Software engineer building performant, thoughtful web experiences. Projects, writing, and more.', + 'Full-stack software engineer building reliable, accessible software across web, mobile, and desktop. Based in Denmark, open to remote roles across the EU.', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - -