Init svelte components

Signed-off-by: Hadi <hadi@example.com>
This commit is contained in:
Hadi
2026-04-28 16:57:00 +02:00
parent f00515e4c3
commit 1025d5bfa1
16 changed files with 1050 additions and 931 deletions
+2 -2
View File
@@ -3,8 +3,8 @@ import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import svelte from '@astrojs/svelte';
// https://astro.build/config
export default defineConfig({
@@ -13,7 +13,7 @@ export default defineConfig({
},
site: "https://hadi.icu",
output: 'static',
integrations: [mdx(), sitemap()],
integrations: [mdx(), sitemap(), svelte()],
vite: {
plugins: [tailwindcss()]
},
+35 -59
View File
@@ -8,6 +8,7 @@
"@astrojs/mdx": "5.0.4",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^8.1.0",
"@lucide/astro": "^0.552.0",
"@tailwindcss/vite": "^4.2.4",
"@types/bun": "^1.3.13",
@@ -15,6 +16,7 @@
"daisyui": "^5.5.19",
"lucide-astro": "^0.556.0",
"node-html-parser": "^7.1.0",
"svelte": "^5.55.5",
"tailwindcss": "^4.2.4",
},
"devDependencies": {
@@ -42,6 +44,8 @@
"@astrojs/sitemap": ["@astrojs/sitemap@3.7.2", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA=="],
"@astrojs/svelte": ["@astrojs/svelte@8.1.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "svelte2tsx": "^0.7.52", "vite": "^7.3.2", "vitefu": "^1.1.2" }, "peerDependencies": { "astro": "^6.0.0", "svelte": "^5.43.6", "typescript": "^5.3.3" } }, "sha512-yZrHRFOxDJeo2hr9rGAMou6/6OL3agEaUCvWNWrea8YhZultsERTYZthfKNC58onAtZs76xNklOYV+G2Dp10kw=="],
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.1", "", { "dependencies": { "ci-info": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw=="],
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.3", "", { "dependencies": { "yaml": "^2.8.2" } }, "sha512-PJzRmgQzUxI2uwpdX2lXSHtP4G8ocp24/t+bZyf5Fy0SZLSF9f9KXZoMlFM/XCGue+B0nH/2IZ7FpBYQATBsCg=="],
@@ -256,6 +260,12 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
@@ -308,6 +318,8 @@
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -412,6 +424,10 @@
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
"dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -458,6 +474,10 @@
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.2.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
@@ -560,6 +580,8 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
@@ -596,6 +618,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
@@ -838,6 +862,8 @@
"sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
@@ -870,6 +896,10 @@
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="],
"svelte2tsx": ["svelte2tsx@0.7.53", "", { "dependencies": { "dedent-js": "^1.0.1", "scule": "^1.3.0" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-ljVSwmnYRDHRm8+7ICP6QoAN7U7vgOFfPBLN6T745YWNYqRRSzHxlrzUVqMjYls2Un8MzJissfziy/38e6Deeg=="],
"svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
"tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="],
@@ -938,7 +968,7 @@
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
@@ -996,6 +1026,8 @@
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
@@ -1026,8 +1058,6 @@
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"astro/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
"bun-types/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
@@ -1058,6 +1088,8 @@
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"svgo/sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@@ -1068,10 +1100,6 @@
"unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"volar-service-typescript/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
@@ -1085,57 +1113,5 @@
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
}
}
+2
View File
@@ -15,6 +15,7 @@
"@astrojs/mdx": "5.0.4",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^8.1.0",
"@lucide/astro": "^0.552.0",
"@tailwindcss/vite": "^4.2.4",
"@types/bun": "^1.3.13",
@@ -22,6 +23,7 @@
"daisyui": "^5.5.19",
"lucide-astro": "^0.556.0",
"node-html-parser": "^7.1.0",
"svelte": "^5.55.5",
"tailwindcss": "^4.2.4"
},
"devDependencies": {
+1 -1
View File
@@ -28,7 +28,7 @@ function isActive(href: string) {
~/hadi
</a>
<div id="oneko-track" class="flex-1 relative h-12 pointer-events-none">
<div id="oneko-track" transition:persist class="flex-1 relative h-12 pointer-events-none">
</div>
<nav class="hidden md:flex items-center">
+278
View File
@@ -0,0 +1,278 @@
<script lang="ts">
import { onMount } from "svelte";
interface GNode {
id: string;
title: string;
current: boolean;
}
interface GEdge {
from: string;
to: string;
}
interface Props {
nodes?: GNode[];
edges?: GEdge[];
}
const { nodes = [], edges = [] }: Props = $props();
const PRIMARY = "oklch(71% 0.0863 296.59)";
let canvas: HTMLCanvasElement;
function startAnimation(): () => void {
if (!canvas || nodes.length === 0) return () => {};
const W = (canvas.width = canvas.offsetWidth);
const H = (canvas.height = 190);
const ctx = canvas.getContext("2d")!;
type SimNode = GNode & { x: number; y: number; vx: number; vy: number };
const simNodes: SimNode[] = nodes.map((n) => ({
...n,
x: n.current ? W / 2 : W / 2 + (Math.random() - 0.5) * 80,
y: n.current ? H / 2 : H / 2 + (Math.random() - 0.5) * 80,
vx: 0,
vy: 0,
}));
let dragging: SimNode | null = null;
let hovered: SimNode | null = null;
function nodeAt(x: number, y: number): SimNode | null {
return (
simNodes.find((n) => {
const dx = n.x - x,
dy = n.y - y;
return Math.sqrt(dx * dx + dy * dy) < (n.current ? 10 : 8);
}) ?? null
);
}
function tick() {
for (let i = 0; i < simNodes.length; i++) {
for (let j = i + 1; j < simNodes.length; j++) {
const a = simNodes[i],
b = simNodes[j];
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = 900 / (d * d);
a.vx -= (dx / d) * f;
a.vy -= (dy / d) * f;
b.vx += (dx / d) * f;
b.vy += (dy / d) * f;
}
}
for (const e of edges) {
const a = simNodes.find((n) => n.id === e.from);
const b = simNodes.find((n) => n.id === e.to);
if (!a || !b) continue;
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = (d - 75) * 0.04;
a.vx += (dx / d) * f;
a.vy += (dy / d) * f;
b.vx -= (dx / d) * f;
b.vy -= (dy / d) * f;
}
for (const n of simNodes) {
n.vx += (W / 2 - n.x) * 0.025;
n.vy += (H / 2 - n.y) * 0.025;
}
for (const n of simNodes) {
if (n === dragging) continue;
n.vx *= 0.78;
n.vy *= 0.78;
n.x = Math.max(16, Math.min(W - 16, n.x + n.vx));
n.y = Math.max(16, Math.min(H - 16, n.y + n.vy));
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = "oklch(2% 0 0)";
ctx.fillRect(0, 0, W, H);
const connected = new Set<string>();
if (hovered) {
for (const e of edges) {
if (e.from === hovered.id) connected.add(e.to);
if (e.to === hovered.id) connected.add(e.from);
}
}
for (const e of edges) {
const a = simNodes.find((n) => n.id === e.from);
const b = simNodes.find((n) => n.id === e.to);
if (!a || !b) continue;
const lit =
hovered && (e.from === hovered.id || e.to === hovered.id);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = lit ? "oklch(55% 0 0)" : "oklch(27% 0 0)";
ctx.lineWidth = lit ? 1.5 : 1;
ctx.stroke();
}
for (const n of simNodes) {
const isHov = hovered?.id === n.id;
const isCon = connected.has(n.id);
const r = n.current ? 7 : isHov ? 6 : 4.5;
if (isHov && !n.current) {
ctx.beginPath();
ctx.arc(n.x, n.y, r + 5, 0, Math.PI * 2);
ctx.fillStyle = "oklch(71% 0.0863 296.59 / 0.15)";
ctx.fill();
}
ctx.beginPath();
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
ctx.fillStyle = n.current
? PRIMARY
: isHov
? "oklch(78% 0.05 296.59)"
: isCon
? "oklch(58% 0.03 296.59)"
: "oklch(40% 0 0)";
ctx.fill();
if (n.current || isHov || isCon) {
ctx.font = `${n.current ? "10px" : "9px"} monospace`;
ctx.textAlign = "center";
ctx.fillStyle = n.current ? "oklch(87% 0 0)" : "oklch(62% 0 0)";
const label =
n.title.length > 14 ? n.title.slice(0, 13) + "…" : n.title;
ctx.fillText(label, n.x, n.y + r + 9);
}
}
}
let animId: number;
function loop() {
tick();
draw();
animId = requestAnimationFrame(loop);
}
animId = requestAnimationFrame(loop);
const onMousedown = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
dragging = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
};
const onMousemove = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const x = (e.clientX - r.left) * sx;
const y = (e.clientY - r.top) * (H / canvas.offsetHeight);
if (dragging) {
dragging.x = x;
dragging.y = y;
dragging.vx = 0;
dragging.vy = 0;
}
hovered = nodeAt(x, y);
canvas.style.cursor =
hovered && !hovered.current ? "pointer" : "default";
};
const onMouseup = () => {
dragging = null;
};
const onMouseleave = () => {
dragging = null;
hovered = null;
};
const onClick = (e: MouseEvent) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const n = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
if (n && !n.current) window.location.href = `/notes/${n.id}`;
};
canvas.addEventListener("mousedown", onMousedown);
canvas.addEventListener("mousemove", onMousemove);
canvas.addEventListener("mouseup", onMouseup);
canvas.addEventListener("mouseleave", onMouseleave);
canvas.addEventListener("click", onClick);
return () => {
cancelAnimationFrame(animId);
canvas.removeEventListener("mousedown", onMousedown);
canvas.removeEventListener("mousemove", onMousemove);
canvas.removeEventListener("mouseup", onMouseup);
canvas.removeEventListener("mouseleave", onMouseleave);
canvas.removeEventListener("click", onClick);
};
}
onMount(() => {
const drawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
const outerDrawer =
drawer?.closest<HTMLElement>(".drawer.drawer-end") ?? null;
let stopFn: (() => void) | null = null;
function isVisible() {
return (
(drawer?.checked ?? false) ||
(outerDrawer?.classList.contains("xl:drawer-open") ?? false)
);
}
function start() {
if (stopFn) return;
stopFn = startAnimation();
}
function stop() {
stopFn?.();
stopFn = null;
}
if (isVisible()) start();
const onDrawerChange = () => {
isVisible() ? start() : stop();
};
drawer?.addEventListener("change", onDrawerChange);
// Watch for xl:drawer-open class toggled by the graph button script
const observer = new MutationObserver(() => {
isVisible() ? start() : stop();
});
if (outerDrawer) {
observer.observe(outerDrawer, {
attributes: true,
attributeFilter: ["class"],
});
}
return () => {
stop();
drawer?.removeEventListener("change", onDrawerChange);
observer.disconnect();
};
});
</script>
<canvas
bind:this={canvas}
height="190"
role="img"
aria-label="Graph of linked notes"
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
></canvas>
+104
View File
@@ -0,0 +1,104 @@
---
import NoteGraph from "./NoteGraph.svelte";
import Author from "./Author.astro";
import { formatDate } from "../utils/notes";
import type { CollectionEntry } from "astro:content";
interface Props {
entry: CollectionEntry<"notes">;
graphNodes: { id: string; title: string; current: boolean }[];
graphEdges: { from: string; to: string }[];
forwardLinks: CollectionEntry<"notes">[];
backlinks: CollectionEntry<"notes">[];
}
const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
---
<aside
id="right-sidebar"
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
style="background: oklch(4% 0 0);"
>
<div class="border-b border-base-300/40">
<p
class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest px-3 pt-3 pb-2"
>
graph
</p>
<NoteGraph client:visible nodes={graphNodes} edges={graphEdges} />
{
graphNodes.length < 2 && (
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
no connections yet
</p>
)
}
</div>
{
forwardLinks.length > 0 && (
<div class="p-3 border-b border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
links
</p>
<ul class="space-y-1">
{forwardLinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
→ {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
backlinks.length > 0 && (
<div class="p-3">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
backlinks
</p>
<ul class="space-y-1">
{backlinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
← {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
forwardLinks.length === 0 && backlinks.length === 0 && (
<div class="p-3">
<p class="font-mono text-[9px] text-base-content/20">
no linked notes
</p>
</div>
)
}
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
<time
datetime={entry.data.publishDate.toISOString()}
class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest"
>
{formatDate(entry.data.publishDate)}
</time>
</div>
<div class="px-4 py-4">
<Author />
</div>
</aside>
+115
View File
@@ -0,0 +1,115 @@
---
import { getCategory, extractInlineHashtags } from "../utils/notes";
import type { CollectionEntry } from "astro:content";
interface Props {
notes: CollectionEntry<"notes">[];
currentEntry: CollectionEntry<"notes">;
categories: string[];
}
const { notes, currentEntry, categories } = Astro.props;
---
<style>
/*
* DaisyUI's menu items use display:grid with grid-auto-columns:minmax(auto,max-content)
* which expands to fit content. Override to block so text-overflow:ellipsis works directly.
* Inner <ul> has no w-full so DaisyUI's margin-inline-start/padding-inline-start don't overflow.
*/
.nav-item {
display: block !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
</style>
<aside
class="w-56 flex flex-col border-r border-base-300/60 h-[calc(100vh-3rem)]"
style="background: oklch(4% 0 0);"
>
<div class="px-3 py-3 border-b border-base-300/40">
<label
class="input input-sm w-full font-mono text-xs border-base-300/40 bg-base-200/50"
>
<span class="text-base-content/30"></span>
<input
data-search
type="text"
placeholder="search..."
class="text-base-content/70 placeholder:text-base-content/25"
/>
</label>
</div>
<ul
class="nav-sidebar menu menu-xs flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 bg-transparent"
>
{
categories.map((cat) => (
<li class="w-full">
<details open={cat === getCategory(currentEntry)}>
<summary class="font-bold tracking-tight text-sm">
<span class="text-primary/50 font-mono">/</span>
{cat}
</summary>
<ul>
{notes
.filter((n) => getCategory(n) === cat)
.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class:list={[
"nav-item font-mono text-xs tooltip tooltip-right",
n.id === currentEntry.id ? "active" : "",
]}
data-tip={n.data.title}
data-title={n.data.title.toLowerCase()}
data-tags={[
...n.data.tags,
...extractInlineHashtags(n.body ?? ""),
].join(",")}
>
{n.data.title}
</a>
</li>
))}
</ul>
</details>
</li>
))
}
</ul>
</aside>
<script>
document.addEventListener("astro:page-load", () => {
const navItems = document.querySelectorAll<HTMLElement>(".nav-item");
document
.querySelectorAll<HTMLInputElement>("[data-search]")
.forEach((input) => {
input.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
const raw = target.value.toLowerCase().trim();
document
.querySelectorAll<HTMLInputElement>("[data-search]")
.forEach((o) => {
if (o !== target) o.value = target.value;
});
const isTag = raw.startsWith("#");
const search = isTag ? raw.slice(1) : raw;
navItems.forEach((item) => {
const title = item.dataset.title ?? "";
const tags = item.dataset.tags ? item.dataset.tags.split(",") : [];
const match =
!search ||
(isTag
? tags.some((t) => t.includes(search))
: title.includes(search) || tags.join(",").includes(search));
item.style.display = match ? "" : "none";
});
});
});
});
</script>
+40
View File
@@ -0,0 +1,40 @@
---
interface Props {
headings: { depth: number; text: string; id: string }[];
}
const { headings } = Astro.props;
---
{
headings.length > 0 && (
<div
class="collapse collapse-arrow mb-8 border border-base-300/40"
style="background: oklch(4% 0 0);"
>
<input type="checkbox" />
<div class="collapse-title font-mono text-xs text-base-content/35 flex items-center gap-2 py-2 px-3 min-h-0">
<span class="text-primary/40">§</span>
table of contents
</div>
<div class="collapse-content px-0 pb-0">
<nav class="px-3 pb-3 pt-1 border-t border-base-300/30 space-y-0.5">
{headings.map((h) => (
<a
href={`#${h.id}`}
class:list={[
"block text-xs text-base-content/45 hover:text-base-content/80 transition-colors py-0.5",
h.depth === 3 ? "pl-4" : h.depth === 4 ? "pl-8" : "",
]}
>
<span class="font-mono text-primary/25 mr-1.5">
{"#".repeat(h.depth)}
</span>
{h.text}
</a>
))}
</nav>
</div>
</div>
)
}
-110
View File
@@ -1,110 +0,0 @@
---
interface Props {
vars: string[];
}
const { vars } = Astro.props;
---
{
vars.length > 0 && (
<>
<button
id="vars-toggle"
data-note-vars={JSON.stringify(vars)}
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
onclick="document.getElementById('vars-modal').showModal()"
>
<span class="font-mono text-[10px] leading-none">$</span>
vars
</button>
<dialog id="vars-modal" class="modal">
<div
class="modal-box max-w-sm"
style="background: oklch(10% 0 0); border: 1px solid oklch(71% 0.0863 296.59 / 0.5);"
>
<h3 class="font-mono text-[10px] text-base-content/40 uppercase tracking-widest mb-4">
variables
</h3>
<div class="space-y-2.5">
{vars.map((v) => (
<div class="flex items-center gap-3">
<label
class="font-mono text-xs text-primary/70 w-36 shrink-0 truncate"
title={`$${v}`}
>
${v}
</label>
<input
type="text"
data-var={v}
placeholder={`$${v}`}
class="vars-input input input-sm flex-1 min-w-0 font-mono text-xs bg-base-300/20 border-base-300/60 text-base-content/80 placeholder:text-base-content/25 focus:border-primary/60"
/>
</div>
))}
</div>
<div class="mt-5 flex justify-end">
<button
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
onclick="document.getElementById('vars-modal').close()"
>
close
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</>
)
}
<script>
document.addEventListener("astro:page-load", () => {
const btn = document.getElementById("vars-toggle");
if (!btn) return;
const vars: string[] = JSON.parse(btn.dataset.noteVars ?? "[]");
const content = document.querySelector(".note-content");
if (!vars.length || !content) return;
const originals = new Map<Text, string>();
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
let node: Text | null;
while ((node = walker.nextNode() as Text | null)) {
if (/\$[a-zA-Z_][a-zA-Z0-9_]*/.test(node.nodeValue ?? "")) {
originals.set(node, node.nodeValue!);
}
}
function applyVars() {
const values: Record<string, string> = {};
document.querySelectorAll<HTMLInputElement>(".vars-input").forEach((input) => {
values[input.dataset.var!] = input.value.trim();
});
const sorted = [...vars].sort((a, b) => b.length - a.length);
for (const [node, original] of originals) {
let text = original;
for (const name of sorted) {
const val = values[name];
if (val) {
text = text.replace(
new RegExp(`\\$${name}(?![a-zA-Z0-9_])`, "g"),
val,
);
}
}
node.nodeValue = text;
}
}
document.addEventListener("input", (e) => {
if ((e.target as HTMLElement)?.classList.contains("vars-input")) applyVars();
});
});
</script>
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import { onMount } from "svelte";
interface Props {
vars: string[];
}
const { vars }: Props = $props();
let values = $state<Record<string, string>>(
Object.fromEntries(vars.map((v) => [v, ""])),
);
let open = $state(false);
let applied = $state(false);
const originals = new Map<Text, string>();
onMount(() => {
const content = document.querySelector(".note-content");
if (!content) return;
const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
let node: Text | null;
while ((node = walker.nextNode() as Text | null)) {
if (/\$[a-zA-Z_][a-zA-Z0-9_]*/.test(node.nodeValue ?? "")) {
originals.set(node, node.nodeValue!);
}
}
});
function applyVars() {
const sorted = [...vars].sort((a, b) => b.length - a.length);
for (const [node, original] of originals) {
let text = original;
for (const name of sorted) {
const val = values[name];
if (val) {
text = text.replace(
new RegExp(`\\$${name}(?![a-zA-Z0-9_])`, "g"),
val,
);
}
}
node.nodeValue = text;
}
applied = true;
setTimeout(() => (applied = false), 1800);
}
</script>
{#if vars.length > 0}
<button
onclick={() => (open = true)}
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
<span class="font-mono text-[10px] leading-none">$</span>
vars
</button>
{#if open}
<dialog class="modal modal-open">
<div
class="modal-box max-w-sm"
style="background: oklch(10% 0 0); border: 1px solid oklch(71% 0.0863 296.59 / 0.5);"
>
<h3
class="font-mono text-[10px] text-base-content/40 uppercase tracking-widest mb-4"
>
variables
</h3>
<div class="space-y-2.5">
{#each vars as v}
<div class="flex items-center gap-3">
<label
class="font-mono text-xs text-primary/70 w-36 shrink-0 truncate"
title={`$${v}`}
>
${v}
</label>
<input
type="text"
bind:value={values[v]}
placeholder={`$${v}`}
class="input input-sm flex-1 min-w-0 font-mono text-xs bg-base-300/20 border-base-300/60 text-base-content/80 placeholder:text-base-content/25 focus:border-primary/60"
/>
</div>
{/each}
</div>
<div class="mt-5 flex items-center justify-between">
<span
class="font-mono text-[10px] text-primary/60 transition-opacity duration-300"
class:opacity-0={!applied}
>
✓ applied
</span>
<div class="flex gap-2">
<button
onclick={applyVars}
class="btn btn-ghost btn-xs font-mono text-primary/60 hover:text-primary border border-primary/30"
>
apply
</button>
<button
onclick={() => (open = false)}
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
>
close
</button>
</div>
</div>
</div>
<button
onclick={() => (open = false)}
class="modal-backdrop"
aria-label="close"
></button>
</dialog>
{/if}
{/if}
+150
View File
@@ -0,0 +1,150 @@
<script lang="ts">
import { onMount } from "svelte";
interface NoteItem {
id: string;
title: string;
description: string;
tags: string[];
category: string;
searchText: string;
}
interface Props {
notes: NoteItem[];
}
const { notes }: Props = $props();
let inputValue = $state("");
const query = $derived(inputValue.toLowerCase().trim());
const categories = $derived([
...new Set(notes.map((n) => n.category)),
].sort());
const filtered = $derived.by(() => {
if (!query) return notes;
const isTag = query.startsWith("#");
const q = isTag ? query.slice(1) : query;
return notes.filter((n) =>
isTag
? n.tags.some((t) => t.includes(q)) || n.searchText.includes(`#${q}`)
: n.searchText.includes(q),
);
});
const visibleCount = $derived(filtered.length);
onMount(() => {
const urlTag = new URLSearchParams(window.location.search).get("tag");
if (urlTag) inputValue = `#${urlTag}`;
});
$effect(() => {
const url = new URL(window.location.href);
if (query.startsWith("#") && query.length > 1) {
url.searchParams.set("tag", query.slice(1));
} else {
url.searchParams.delete("tag");
}
history.replaceState(null, "", url.toString());
});
function filteredByCategory(cat: string) {
return filtered.filter((n) => n.category === cat);
}
</script>
<div class="mb-12 max-w-sm mx-auto">
<label class="input w-full">
<span class="text-base-content/25"></span>
<input
type="text"
placeholder="search or #tag..."
bind:value={inputValue}
/>
</label>
<p class="font-mono text-[10px] text-base-content/20 mt-1.5 text-center">
use #tag to filter by tag
</p>
</div>
<div class="space-y-12">
{#each categories as cat}
{@const catNotes = filteredByCategory(cat)}
{#if catNotes.length > 0}
<section>
<div class="flex items-baseline gap-3 mb-4">
<h2 class="text-xl font-bold tracking-tight">
<span class="text-primary/50 font-mono mr-1">/</span>{cat}
</h2>
<span class="font-mono text-xs text-base-content/25">
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
</span>
</div>
<div class="border-t border-base-300/40 mb-1"></div>
<ul class="divide-y divide-base-300/20">
{#each catNotes as n}
<li>
<a
href={`/notes/${n.id}`}
class="group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
>
<div class="flex-1 min-w-0">
<div class="flex flex-col mb-0.5">
<span
class="font-semibold text-sm group-hover:text-primary transition-colors"
>
{n.title}
</span>
{#if n.description}
<span class="text-xs text-base-content/35 truncate">
{n.description}
</span>
{/if}
</div>
{#if n.tags.length > 0}
<div class="flex flex-wrap gap-1 mt-1">
{#each n.tags as tag}
<span
class="badge badge-ghost badge-xs font-mono text-base-content/30"
>
{tag}
</span>
{/each}
</div>
{/if}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
>
<path d="m9 18 6-6-6-6" />
</svg>
</a>
</li>
{/each}
</ul>
</section>
{/if}
{/each}
</div>
{#if visibleCount === 0}
<div class="text-center py-20 font-mono text-sm text-base-content/25">
no results.
</div>
{/if}
<p class="text-center font-mono text-xs text-base-content/20 mt-16">
{visibleCount} note{visibleCount !== 1 ? "s" : ""} total
</p>
+147 -252
View File
@@ -2,9 +2,15 @@
import { getCollection, render } from "astro:content";
import Layout from "../../layouts/Layout.astro";
import { List, PanelRight } from "@lucide/astro";
import Author from "../../components/Author.astro";
import NoteVars from "../../components/NoteVars.astro";
import { getCategory, extractInlineHashtags } from "../../utils/notes";
import NoteTOC from "../../components/NoteTOC.astro";
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
import NoteVars from "../../components/NoteVars.svelte";
import {
getCategory,
extractLinks,
extractHeadings,
} from "../../utils/notes";
export async function getStaticPaths() {
const notes = await getCollection("notes");
@@ -23,22 +29,6 @@ const sortedNotes = allNotes.sort((a, b) =>
);
const categories = [...new Set(allNotes.map(getCategory))].sort();
function formatDate(date: Date) {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
function extractLinks(body: string): string[] {
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
const ids: string[] = [];
let m;
while ((m = re.exec(body)) !== null) ids.push(m[1]);
return [...new Set(ids)];
}
const allLinks = Object.fromEntries(
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
);
@@ -61,15 +51,6 @@ const graphEdges = [
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
];
// Mirrors github-slugger: keeps _, keeps unicode letters/numbers, spaces → hyphens
function slugify(text: string) {
return text
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.trim()
.replace(/ +/g, "-");
}
const noteVars = [
...new Set(
Array.from(
@@ -79,20 +60,7 @@ const noteVars = [
),
];
const headings: { depth: number; text: string; id: string }[] = [];
const headingRe = /^(#{2,4}) (.+)$/gm;
let hm;
while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
const raw = hm[2]
.trim()
.replace(/`[^`]*`/g, "")
.replace(/\*\*(.*?)\*\*/g, "$1")
.replace(/(?<!\p{L}\p{N})__(.*?)__(?!\p{L}\p{N})/gu, "$1")
.replace(/\*(.*?)\*/g, "$1")
.replace(/(?<!\p{L}\p{N})_(.*?)_(?!\p{L}\p{N})/gu, "$1")
.replace(/[*]/g, "");
headings.push({ depth: hm[1].length, text: raw, id: slugify(raw) });
}
const headings = extractHeadings(entry.body ?? "");
---
<style>
@@ -101,26 +69,6 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
top: 3rem;
height: calc(100vh - 3rem);
}
/* DaisyUI .menu forces width:fit-content and display:grid on items.
Astro scoped styles add [data-astro-cid-*] which raises specificity above DaisyUI's class selectors. */
.nav-sidebar {
width: 100%;
}
.nav-sidebar a {
display: flex;
overflow: hidden;
min-width: 0;
}
.nav-sidebar a span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
flex: 1;
}
</style>
<Layout
@@ -139,9 +87,16 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
<div class="max-w-3xl mx-auto lg:mx-0">
<div class="flex items-center justify-between mb-6">
<div class="breadcrumbs text-xs font-mono text-base-content/35 p-0">
<div
class="breadcrumbs text-xs font-mono text-base-content/35 p-0"
>
<ul>
<li><a href="/notes" class="hover:text-base-content/70">notes</a></li>
<li>
<a
href="/notes"
class="hover:text-base-content/70"
>notes</a>
</li>
<li>{getCategory(entry)}</li>
</ul>
</div>
@@ -153,7 +108,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<List size={11} />
nav
</label>
<NoteVars vars={noteVars} />
<NoteVars client:load vars={noteVars} />
<label
for="graph-drawer"
id="graph-toggle"
@@ -167,7 +122,9 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
</div>
<header class="mb-8">
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">
<h1
class="text-4xl sm:text-5xl font-bold tracking-tight mb-3"
>
{entry.data.title}
</h1>
<p class="text-base-content/50 mb-4">
@@ -189,38 +146,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
}
</header>
{
headings.length > 0 && (
<div
class="collapse collapse-arrow mb-8 border border-base-300/40"
style="background: oklch(4% 0 0);"
>
<input type="checkbox" />
<div class="collapse-title font-mono text-xs text-base-content/35 flex items-center gap-2 py-2 px-3 min-h-0">
<span class="text-primary/40">§</span>
table of contents
</div>
<div class="collapse-content px-0 pb-0">
<nav class="px-3 pb-3 pt-1 border-t border-base-300/30 space-y-0.5">
{headings.map((h) => (
<a
href={`#${h.id}`}
class:list={[
"block text-xs text-base-content/45 hover:text-base-content/80 transition-colors py-0.5",
h.depth === 3 ? "pl-4" : h.depth === 4 ? "pl-8" : "",
]}
>
<span class="font-mono text-primary/25 mr-1.5">
{"#".repeat(h.depth)}
</span>
{h.text}
</a>
))}
</nav>
</div>
</div>
)
}
<NoteTOC headings={headings} />
<div
class="note-content text-sm leading-relaxed text-base-content/80
@@ -246,11 +172,19 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<Content />
</div>
<div class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25">
<a href="/notes" class="hover:text-base-content/50 transition-colors">
<div
class="border-t border-base-300/30 mt-12 pt-6 flex items-center justify-between font-mono text-[10px] text-base-content/25"
>
<a
href="/notes"
class="hover:text-base-content/50 transition-colors"
>
← all notes
</a>
<a href="/" class="hover:text-base-content/50 transition-colors">
<a
href="/"
class="hover:text-base-content/50 transition-colors"
>
~/hadi
</a>
</div>
@@ -264,58 +198,11 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
aria-label="close sidebar"
class="drawer-overlay"
></label>
<aside
class="w-56 flex flex-col border-r border-base-300/60 h-full"
style="background: oklch(4% 0 0);"
>
<div class="px-3 py-3 border-b border-base-300/40">
<label class="input input-sm w-full font-mono text-xs border-base-300/40 bg-base-200/50">
<span class="text-base-content/30"></span>
<input
data-search
type="text"
placeholder="search..."
class="text-base-content/70 placeholder:text-base-content/25"
<NoteNavSidebar
notes={sortedNotes}
currentEntry={entry}
categories={categories}
/>
</label>
</div>
<ul class="nav-sidebar menu menu-xs flex-1 overflow-y-auto px-2 py-2 bg-transparent">
{
categories.map((cat) => (
<li class="w-full">
<details open={cat === getCategory(entry)}>
<summary class="font-bold tracking-tight text-sm">
<span class="text-primary/50 font-mono">/</span>{cat}
</summary>
<ul class="w-full">
{sortedNotes
.filter((n) => getCategory(n) === cat)
.map((n) => (
<li class="w-full">
<a
href={`/notes/${n.id}`}
class:list={[
"nav-item font-mono text-xs tooltip tooltip-right",
n.id === entry.id ? "active" : "",
]}
data-tip={n.data.title}
data-title={n.data.title.toLowerCase()}
data-tags={[
...n.data.tags,
...extractInlineHashtags(n.body ?? ""),
].join(",")}
>
<span>{n.data.title}</span>
</a>
</li>
))}
</ul>
</details>
</li>
))
}
</ul>
</aside>
</div>
</div>
</div>
@@ -326,107 +213,115 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
aria-label="close sidebar"
class="drawer-overlay xl:hidden"
></label>
<aside
id="right-sidebar"
class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
style="background: oklch(4% 0 0);"
>
<div class="border-b border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest px-3 pt-3 pb-2">
graph
</p>
<canvas
id="note-graph"
height="190"
role="img"
aria-label="Graph of linked notes"
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
></canvas>
{
graphNodes.length < 2 && (
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
no connections yet
</p>
)
}
</div>
{
forwardLinks.length > 0 && (
<div class="p-3 border-b border-base-300/40">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
links
</p>
<ul class="space-y-1">
{forwardLinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
→ {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
backlinks.length > 0 && (
<div class="p-3">
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
backlinks
</p>
<ul class="space-y-1">
{backlinks.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
>
← {n.data.title}
</a>
</li>
))}
</ul>
</div>
)
}
{
forwardLinks.length === 0 && backlinks.length === 0 && (
<div class="p-3">
<p class="font-mono text-[9px] text-base-content/20">
no linked notes
</p>
</div>
)
}
<div class="px-4 pt-4 pb-1 border-t border-base-300/40">
<time
datetime={entry.data.publishDate.toISOString()}
class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest"
>
{formatDate(entry.data.publishDate)}
</time>
</div>
<div class="px-4 py-4">
<Author />
</div>
</aside>
<NoteGraphSidebar
entry={entry}
graphNodes={graphNodes}
graphEdges={graphEdges}
forwardLinks={forwardLinks}
backlinks={backlinks}
/>
</div>
</div>
<script is:inline define:vars={{ graphNodes, graphEdges }}>
window.__graphNodes = graphNodes;
window.__graphEdges = graphEdges;
</script>
<script>
import "../../utils/notes-graph.ts";
</script>
</main>
</Layout>
<script>
function injectHeadingAnchors() {
if (!document.getElementById("heading-anchor-styles")) {
const s = document.createElement("style");
s.id = "heading-anchor-styles";
s.textContent = `
.note-content h2, .note-content h3, .note-content h4 {
display: flex !important;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.heading-anchor {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-left: 0.4em;
color: oklch(38% 0 0);
opacity: 0;
transition: opacity 120ms, color 120ms;
text-decoration: none;
}
.note-content h2:hover .heading-anchor,
.note-content h3:hover .heading-anchor,
.note-content h4:hover .heading-anchor { opacity: 1; }
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
`;
document.head.appendChild(s);
}
document
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
.forEach((heading) => {
if (!heading.id || heading.querySelector(".heading-anchor")) return;
const anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.className = "heading-anchor";
anchor.setAttribute("aria-label", "Copy link to section");
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
anchor.addEventListener("click", (e) => {
e.preventDefault();
const url = `${location.origin}${location.pathname}#${heading.id}`;
navigator.clipboard.writeText(url).then(() => {
anchor.classList.add("copied");
setTimeout(() => anchor.classList.remove("copied"), 1800);
});
history.pushState(null, "", `#${heading.id}`);
});
heading.appendChild(anchor);
});
}
function initXlGraphToggle() {
const graphDrawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
if (!graphDrawer) return;
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
const xlQuery = window.matchMedia("(min-width: 1280px)");
const STORAGE_KEY = "notes-graph-sidebar";
function setXlSidebar(open: boolean) {
if (!outerDrawer) return;
if (open) {
outerDrawer.classList.add("xl:drawer-open");
} else {
outerDrawer.classList.remove("xl:drawer-open");
}
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
}
const graphToggle = document.getElementById("graph-toggle");
graphToggle?.addEventListener("click", (e) => {
if (!xlQuery.matches) return;
e.preventDefault();
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
});
if (xlQuery.matches) {
const saved = localStorage.getItem(STORAGE_KEY);
// Open by default unless user explicitly closed it
setXlSidebar(saved !== "0");
}
xlQuery.addEventListener("change", (e) => {
if (!e.matches) {
outerDrawer?.classList.remove("xl:drawer-open");
} else {
const saved = localStorage.getItem(STORAGE_KEY);
setXlSidebar(saved !== "0");
}
});
}
document.addEventListener("astro:page-load", () => {
injectHeadingAnchors();
initXlGraphToggle();
});
</script>
+12 -149
View File
@@ -1,22 +1,24 @@
---
import Layout from "../../layouts/Layout.astro";
import { getCollection } from "astro:content";
import { ChevronRight, Shield } from "@lucide/astro";
import { getCategory } from "../../utils/notes";
import NotesSearch from "../../components/NotesSearch.svelte";
const notes = await getCollection("notes");
const sortedNotes = notes.sort(
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
);
const categories = [...new Set(notes.map(getCategory))].sort();
const searchIndex = Object.fromEntries(
sortedNotes.map((n) => [
n.id,
[n.data.title, n.data.description, n.body ?? ""].join(" ").toLowerCase(),
]),
);
const searchNotes = sortedNotes.map((n) => ({
id: n.id,
title: n.data.title,
description: n.data.description,
tags: n.data.tags,
category: getCategory(n),
searchText: [n.data.title, n.data.description, n.body ?? ""]
.join(" ")
.toLowerCase(),
}));
---
<Layout
@@ -30,145 +32,6 @@ const searchIndex = Object.fromEntries(
Reference sheets on cybersecurity tools and techniques.
</p>
</div>
<div class="mb-12 max-w-sm mx-auto">
<label class="input w-full">
<span class="text-base-content/25"></span>
<input data-search type="text" placeholder="search or #tag..." />
</label>
<p class="font-mono text-[10px] text-base-content/20 mt-1.5 text-center">
use #tag to filter by tag
</p>
</div>
<div id="notes-container" class="space-y-12">
{
categories.map((cat) => {
const catNotes = sortedNotes.filter((n) => getCategory(n) === cat);
return (
<section data-category={cat.toLowerCase()}>
<div class="flex items-baseline gap-3 mb-4">
<h2 class="text-xl font-bold tracking-tight">
<span class="text-primary/50 font-mono mr-1">/</span>
{cat}
</h2>
<span class="font-mono text-xs text-base-content/25">
{catNotes.length} note{catNotes.length !== 1 ? "s" : ""}
</span>
</div>
<div class="border-t border-base-300/40 mb-1" />
<ul class="divide-y divide-base-300/20">
{catNotes.map((n) => (
<li>
<a
href={`/notes/${n.id}`}
class="note-card group flex items-center gap-4 py-3 hover:bg-base-200/30 px-2 -mx-2 transition-colors"
data-id={n.id}
data-tags={n.data.tags.join(",")}
>
<div class="flex-1 min-w-0">
<div class="flex flex-col mb-0.5">
<span class="font-semibold text-sm group-hover:text-primary transition-colors">
{n.data.title}
</span>
{n.data.description && (
<span class="text-xs text-base-content/35 truncate">
{n.data.description}
</span>
)}
</div>
{n.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mt-1">
{n.data.tags.map((tag) => (
<span class="badge badge-ghost badge-xs font-mono text-base-content/30">
{tag}
</span>
))}
</div>
)}
</div>
<ChevronRight
size={14}
class="text-base-content/20 group-hover:text-primary/50 shrink-0 transition-colors"
/>
</a>
</li>
))}
</ul>
</section>
);
})
}
</div>
<div
id="empty-state"
class="hidden text-center py-20 font-mono text-sm text-base-content/25"
>
no results.
</div>
<p class="text-center font-mono text-xs text-base-content/20 mt-16">
<span id="note-count">{notes.length}</span> note{
notes.length !== 1 ? "s" : ""
} total
</p>
<NotesSearch client:load notes={searchNotes} />
</main>
<script is:inline define:vars={{ searchIndex }}>
function init() {
const noteCards = document.querySelectorAll(".note-card");
const sections = document.querySelectorAll("[data-category]");
const emptyState = document.getElementById("empty-state");
const noteCount = document.getElementById("note-count");
const container = document.getElementById("notes-container");
function filter(raw) {
const isTag = raw.startsWith("#");
const query = isTag ? raw.slice(1) : raw;
let visible = 0;
noteCards.forEach((card) => {
const id = card.dataset.id ?? "";
const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
const show =
!query ||
(isTag
? tags.some((t) => t.includes(query)) ||
(searchIndex[id] ?? "").includes(`#${query}`)
: (searchIndex[id] ?? "").includes(query));
card.style.display = show ? "" : "none";
if (show) visible++;
});
sections.forEach((section) => {
const anyVisible = [...section.querySelectorAll(".note-card")].some(
(c) => c.style.display !== "none",
);
section.style.display = anyVisible ? "" : "none";
});
noteCount.textContent = String(visible);
container.style.display = visible > 0 ? "" : "none";
emptyState.classList.toggle("hidden", visible > 0);
}
document.querySelectorAll("[data-search]").forEach((input) => {
input.addEventListener("input", (e) => {
filter(e.target.value.toLowerCase().trim());
});
});
const urlTag = new URLSearchParams(window.location.search).get("tag");
if (urlTag) {
document.querySelectorAll("[data-search]").forEach((i) => {
i.value = `#${urlTag}`;
});
filter(`#${urlTag}`);
}
}
document.addEventListener("astro:page-load", init);
</script>
</Layout>
-356
View File
@@ -1,356 +0,0 @@
const PRIMARY = "oklch(71% 0.0863 296.59)";
type GNode = {
id: string;
title: string;
current: boolean;
x: number;
y: number;
vx: number;
vy: number;
};
type GEdge = { from: string; to: string };
let stopGraph: (() => void) | null = null;
function startGraph(): (() => void) | null {
const w = window as typeof window & {
__graphNodes?: { id: string; title: string; current: boolean }[];
__graphEdges?: GEdge[];
};
const graphNodes = w.__graphNodes ?? [];
const graphEdges: GEdge[] = w.__graphEdges ?? [];
const canvas = document.getElementById(
"note-graph",
) as HTMLCanvasElement | null;
if (!canvas || graphNodes.length === 0) return null;
const W = (canvas.width = canvas.offsetWidth);
const H = (canvas.height = 190);
const ctx = canvas.getContext("2d")!;
const nodes: GNode[] = graphNodes.map((n) => ({
...n,
x: n.current ? W / 2 : W / 2 + (Math.random() - 0.5) * 80,
y: n.current ? H / 2 : H / 2 + (Math.random() - 0.5) * 80,
vx: 0,
vy: 0,
}));
let dragging: GNode | null = null;
let hovered: GNode | null = null;
function nodeAt(x: number, y: number): GNode | null {
return (
nodes.find((n) => {
const dx = n.x - x,
dy = n.y - y;
return Math.sqrt(dx * dx + dy * dy) < (n.current ? 10 : 8);
}) ?? null
);
}
function tick() {
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i],
b = nodes[j];
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = 900 / (d * d);
a.vx -= (dx / d) * f;
a.vy -= (dy / d) * f;
b.vx += (dx / d) * f;
b.vy += (dy / d) * f;
}
}
for (const e of graphEdges) {
const a = nodes.find((n: GNode) => n.id === e.from);
const b = nodes.find((n: GNode) => n.id === e.to);
if (!a || !b) continue;
const dx = b.x - a.x,
dy = b.y - a.y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
const f = (d - 75) * 0.04;
a.vx += (dx / d) * f;
a.vy += (dy / d) * f;
b.vx -= (dx / d) * f;
b.vy -= (dy / d) * f;
}
for (const n of nodes) {
n.vx += (W / 2 - n.x) * 0.025;
n.vy += (H / 2 - n.y) * 0.025;
}
for (const n of nodes) {
if (n === dragging) continue;
n.vx *= 0.78;
n.vy *= 0.78;
n.x = Math.max(16, Math.min(W - 16, n.x + n.vx));
n.y = Math.max(16, Math.min(H - 16, n.y + n.vy));
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = "oklch(2% 0 0)";
ctx.fillRect(0, 0, W, H);
const connected = new Set<string>();
if (hovered) {
for (const e of graphEdges) {
if (e.from === hovered.id) connected.add(e.to);
if (e.to === hovered.id) connected.add(e.from);
}
}
for (const e of graphEdges) {
const a = nodes.find((n: GNode) => n.id === e.from);
const b = nodes.find((n: GNode) => n.id === e.to);
if (!a || !b) continue;
const lit = hovered && (e.from === hovered.id || e.to === hovered.id);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = lit ? "oklch(55% 0 0)" : "oklch(27% 0 0)";
ctx.lineWidth = lit ? 1.5 : 1;
ctx.stroke();
}
for (const n of nodes) {
const isHov = hovered?.id === n.id;
const isCon = connected.has(n.id);
const r = n.current ? 7 : isHov ? 6 : 4.5;
if (isHov && !n.current) {
ctx.beginPath();
ctx.arc(n.x, n.y, r + 5, 0, Math.PI * 2);
ctx.fillStyle = "oklch(71% 0.0863 296.59 / 0.15)";
ctx.fill();
}
ctx.beginPath();
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
ctx.fillStyle = n.current
? PRIMARY
: isHov
? "oklch(78% 0.05 296.59)"
: isCon
? "oklch(58% 0.03 296.59)"
: "oklch(40% 0 0)";
ctx.fill();
if (n.current || isHov || isCon) {
ctx.font = `${n.current ? "10px" : "9px"} monospace`;
ctx.textAlign = "center";
ctx.fillStyle = n.current ? "oklch(87% 0 0)" : "oklch(62% 0 0)";
const label =
n.title.length > 14 ? n.title.slice(0, 13) + "…" : n.title;
ctx.fillText(label, n.x, n.y + r + 9);
}
}
}
let animId: number;
function loop() {
tick();
draw();
animId = requestAnimationFrame(loop);
}
animId = requestAnimationFrame(loop);
canvas.addEventListener("mousedown", (e) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
dragging = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
});
canvas.addEventListener("mousemove", (e) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const x = (e.clientX - r.left) * sx;
const y = (e.clientY - r.top) * (H / canvas.offsetHeight);
if (dragging) {
dragging.x = x;
dragging.y = y;
dragging.vx = 0;
dragging.vy = 0;
}
hovered = nodeAt(x, y);
canvas.style.cursor =
hovered && !hovered.current ? "pointer" : "default";
});
canvas.addEventListener("mouseup", () => {
dragging = null;
});
canvas.addEventListener("mouseleave", () => {
dragging = null;
hovered = null;
});
canvas.addEventListener("click", (e) => {
const r = canvas.getBoundingClientRect();
const sx = W / canvas.offsetWidth;
const n = nodeAt(
(e.clientX - r.left) * sx,
(e.clientY - r.top) * (H / canvas.offsetHeight),
);
if (n && !n.current) window.location.href = `/notes/${n.id}`;
});
return () => cancelAnimationFrame(animId);
}
function injectHeadingAnchors() {
if (!document.getElementById("heading-anchor-styles")) {
const s = document.createElement("style");
s.id = "heading-anchor-styles";
s.textContent = `
.note-content h2, .note-content h3, .note-content h4 {
display: flex !important;
align-items: center;
flex-wrap: wrap;
gap: 0;
}
.heading-anchor {
display: inline-flex;
align-items: center;
flex-shrink: 0;
margin-left: 0.4em;
color: oklch(38% 0 0);
opacity: 0;
transition: opacity 120ms, color 120ms;
text-decoration: none;
}
.note-content h2:hover .heading-anchor,
.note-content h3:hover .heading-anchor,
.note-content h4:hover .heading-anchor { opacity: 1; }
.heading-anchor:hover, .heading-anchor.copied { color: oklch(71% 0.0863 296.59); opacity: 1; }
`;
document.head.appendChild(s);
}
document
.querySelectorAll(".note-content h2, .note-content h3, .note-content h4")
.forEach((heading) => {
if (!heading.id || heading.querySelector(".heading-anchor")) return;
const anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.className = "heading-anchor";
anchor.setAttribute("aria-label", "Copy link to section");
anchor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
anchor.addEventListener("click", (e) => {
e.preventDefault();
const url = `${location.origin}${location.pathname}#${heading.id}`;
navigator.clipboard.writeText(url).then(() => {
anchor.classList.add("copied");
setTimeout(() => anchor.classList.remove("copied"), 1800);
});
history.pushState(null, "", `#${heading.id}`);
});
heading.appendChild(anchor);
});
}
function initSearch() {
const navItems = document.querySelectorAll<HTMLElement>(".nav-item");
document
.querySelectorAll<HTMLInputElement>("[data-search]")
.forEach((input) => {
input.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
const raw = target.value.toLowerCase().trim();
document
.querySelectorAll<HTMLInputElement>("[data-search]")
.forEach((o) => {
if (o !== target) o.value = target.value;
});
const isTag = raw.startsWith("#");
const search = isTag ? raw.slice(1) : raw;
navItems.forEach((item) => {
const title = item.dataset.title ?? "";
const tags = item.dataset.tags ? item.dataset.tags.split(",") : [];
const match =
!search ||
(isTag
? tags.some((t) => t.includes(search))
: title.includes(search) || tags.join(",").includes(search));
item.style.display = match ? "" : "none";
});
});
});
}
function init() {
if (stopGraph) {
stopGraph();
stopGraph = null;
}
const graphDrawer = document.getElementById(
"graph-drawer",
) as HTMLInputElement | null;
if (!graphDrawer) return;
function onGraphDrawerChange() {
if (graphDrawer!.checked) {
requestAnimationFrame(() => {
stopGraph = startGraph() ?? null;
});
} else {
if (stopGraph) {
stopGraph();
stopGraph = null;
}
}
}
graphDrawer.addEventListener("change", onGraphDrawerChange);
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
const xlQuery = window.matchMedia("(min-width: 1280px)");
function setXlSidebar(open: boolean) {
if (!outerDrawer) return;
if (open) {
outerDrawer.classList.add("xl:drawer-open");
requestAnimationFrame(() => {
stopGraph = startGraph() ?? null;
});
} else {
outerDrawer.classList.remove("xl:drawer-open");
if (stopGraph) {
stopGraph();
stopGraph = null;
}
}
}
const graphToggle = document.getElementById("graph-toggle");
graphToggle?.addEventListener("click", (e) => {
if (!xlQuery.matches) return;
e.preventDefault();
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
});
if (xlQuery.matches) {
outerDrawer?.classList.add("xl:drawer-open");
requestAnimationFrame(() => {
stopGraph = startGraph() ?? null;
});
}
xlQuery.addEventListener("change", (e) => {
if (!e.matches) setXlSidebar(false);
});
injectHeadingAnchors();
initSearch();
}
document.addEventListener("astro:page-load", init);
document.addEventListener("astro:before-preparation", () => {
if (stopGraph) {
stopGraph();
stopGraph = null;
}
});
+45
View File
@@ -14,3 +14,48 @@ export function extractInlineHashtags(body: string): string[] {
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
return [...new Set(tags)];
}
// Mirrors github-slugger: keeps _, keeps unicode letters/numbers, spaces → hyphens
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.trim()
.replace(/ +/g, "-");
}
export function extractLinks(body: string): string[] {
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
const ids: string[] = [];
let m;
while ((m = re.exec(body)) !== null) ids.push(m[1]);
return [...new Set(ids)];
}
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
export function extractHeadings(
body: string,
): { depth: number; text: string; id: string }[] {
const headings: { depth: number; text: string; id: string }[] = [];
const re = /^(#{2,4}) (.+)$/gm;
let m;
while ((m = re.exec(body)) !== null) {
const raw = m[2]
.trim()
.replace(/`[^`]*`/g, "")
.replace(/\*\*(.*?)\*\*/g, "$1")
.replace(/(?<!\p{L}\p{N})__(.*?)__(?!\p{L}\p{N})/gu, "$1")
.replace(/\*(.*?)\*/g, "$1")
.replace(/(?<!\p{L}\p{N})_(.*?)_(?!\p{L}\p{N})/gu, "$1")
.replace(/[*]/g, "");
headings.push({ depth: m[1].length, text: raw, id: slugify(raw) });
}
return headings;
}
-1
View File
@@ -1,3 +1,2 @@
- Fix left sidebar overflow/minwidth issue
- Transform to svelte 5 components
- The cat just TP